Robotics Software Engineer Interview Questions
Robotics Software Engineer Interview Questions and Answers
This page is for engineers preparing for robotics software interviews: graduates moving from a robotics degree into industry, software engineers moving into robots, and experienced robotics engineers going for senior roles. The role page Robotics Software Engineer sets out what to learn and in what order; this page is the question bank you practise against once you have started.
Robotics loops usually have four kinds of technical round. A coding round, often in C++, sometimes with a geometry or estimation twist. A domain round that moves through ROS 2, frames and transforms, kinematics, control, estimation, perception and planning, checking that you understand the maths behind the libraries. A system design round about a whole robot or a fleet. And a debugging or experience round, where the interviewer describes a robot misbehaving and listens to how you narrow it down.
The page follows that shape: ten sections of questions from middleware to deployment, each with a model answer and a likely follow-up; ten coding exercises with tested solutions; four scenarios with structured walk-throughs; then a mock interview plan, a cheat sheet, red flags and a short FAQ. For language depth, use Modern C++ Roadmap and Python Roadmap, and for the operating system underneath most robots, Linux Roadmap.
- Questions
- 61 with model answers
- Coding
- 10 exercises with solutions
- Scenarios
- 4 design and debugging rounds
How to use this page
Read a question, say your answer out loud in under two minutes, then compare it with the model answer. Note what you missed, not what you got right.
For every equation in an answer, write it on paper and check the units and frames. Interviewers in this field often ask you to derive rather than recite.
Do the coding exercises in a plain editor with a timer, then write your own test cases before you read the solution. Practise more general problems in coding practice.
Work through each scenario on a whiteboard or paper: requirements first, then the block diagram, then failure modes and tests. Rehearse design rounds with system design practice.
In the last week, run the mock interview plan twice and revise only the cheat sheet and the red flags.
ROS 2 architecture and communication
Interviewers check that you know what ROS 2 does underneath the API, because most field problems with it are discovery, QoS or interface problems rather than code bugs.
How is ROS 2 built on DDS, and what changed compared with ROS 1?Junior
ROS 2 sits on a middleware interface layer, rmw, with DDS implementations underneath. DDS provides peer-to-peer discovery, serialisation and transport, so there is no central master process as in ROS 1: if any one node dies, the others keep talking. That removes a single point of failure and lets nodes start in any order.
Other changes follow from DDS. Every publisher and subscriber has quality of service settings, so you can choose best-effort or reliable delivery per stream. Domains, set by the domain identifier, separate systems that share a network. The client libraries, rclcpp and rclpy, share a common C library, rcl, so behaviour is more consistent between languages. The cost is more configuration: discovery and QoS mismatches are now your problem.
Likely follow-up. What does the rmw layer let you change without touching node code?
Walk me through what happens between calling publish on a ROS 2 publisher and a subscriber callback running on another machine.Mid
Long before the publish, discovery has run: each participant announced itself, the endpoints exchanged topic name, type and QoS, and the publisher matched the subscriber because the profiles were compatible. On publish, rclcpp passes the message through rcl to the rmw implementation, which serialises it, usually to CDR, and hands it to the DDS data writer. DDS sends it over UDP, fragmenting large samples, and with reliable QoS it keeps samples in the writer history until acknowledged.
On the receiving side, the data reader stores the sample in its history queue, which triggers a guard condition in the wait set. The executor wakes, takes the message, deserialises it into the subscriber's type and calls the callback on whichever thread the executor chose. Each step can add latency: serialisation of large messages, network fragmentation and retransmission, and above all waiting for a busy executor.
Likely follow-up. Where would you measure to find which step adds latency?
What is a lifecycle node, and when is it worth the extra code?Mid
A lifecycle, or managed, node has explicit states: unconfigured, inactive, active and finalised, with transitions such as configure, activate, deactivate, cleanup and shutdown. You put resource allocation in configure, start publishing and processing in activate, and undo each step in the matching transition. Lifecycle publishers drop messages while the node is inactive.
It is worth it when bring-up order matters. A navigation stack should not start planning before the map and localisation are ready; a driver should not command motors until its configuration has been checked. An external manager moves the nodes through their transitions in a known order and can bring the system to a known inactive state on error, which is far easier to reason about than nodes that each start as soon as their process runs. For a small utility node with no dependencies, the extra states only add code.
How would you design the message interfaces for a new subsystem, such as a gripper?Senior
First, reuse standard messages where they fit. Joint states, poses, twists and sensor messages are understood by existing tools, so visualisation and recording work for free. For new messages, every time-varying message gets a header with a timestamp and a frame identifier, values use SI units, and frames follow the usual right-handed convention with x forward.
For the gripper I would expose a state topic with position, effort and a status code, and an action for a grasp, because grasping takes time and needs feedback and cancellation. Status codes are constants in the message definition, not free strings. Where an estimate has uncertainty, I include a covariance. I keep messages small and flat, because large nested messages cost serialisation time.
The hard part is change. A message definition is a contract: changing a field breaks recorded bags and every node built against the old type, so I add new messages or fields deliberately, keep interfaces in their own package and review changes to it like an API change.
What goes wrong when you replay recorded bag data into your nodes?Mid
The common problems are about time and QoS. Nodes must use simulated time, reading the clock topic that the player publishes, otherwise a node compares a recorded stamp from last week with the current wall clock and drops everything as stale. Any node that uses a wall-clock timer or sleeps on system time will behave differently during replay, and playing faster than real time exposes callbacks that were only just keeping up.
QoS matters too. Static transforms and maps are published with transient-local durability; if the recording or playback does not preserve that, late-starting nodes never receive them. Replay is also not deterministic with live processing nodes: executor scheduling and thread timing differ each run, so the outputs vary slightly. For regression tests I either compare outputs with tolerances or run the algorithm directly on the recorded messages without the middleware, which is repeatable.
Nodes on a robot and a base-station laptop cannot see each other over Wi-Fi. What is going on and how do you fix it?Senior
Default discovery uses multicast, which many Wi-Fi access points filter or rate-limit, and which corporate networks often block between subnets. First I check the basics: same domain identifier, no localhost-only setting, the firewall allowing the UDP port range, and the right network interface chosen when the robot has several.
If multicast is the problem, I configure discovery so it does not depend on it: a list of unicast peers, or a discovery server that participants register with.
Even with discovery fixed, Wi-Fi changes the design. Reliable QoS on a large topic over a lossy link causes retransmission storms and latency, so I send only compressed or reduced data off the robot, use best-effort for telemetry, and never place anything safety-related on the wireless link. Ideally a bridge node exposes a small, defined set of topics to the outside.
Likely follow-up. What would you put on the robot side of the link and what on the laptop?
Executors, QoS, timing and tf2
These questions test whether you can predict when your callbacks run and whether the data you combine refers to the same instant and the same frame.
What does an executor do, and how do the single-threaded and multi-threaded executors differ?Junior
An executor owns a wait set for the subscriptions, timers, services and clients of the nodes added to it. When spin is called, it waits until something is ready, picks a ready entity and runs its callback, then waits again. A single-threaded executor runs one callback at a time, in the spinning thread, so callbacks never overlap and need no locking between them. A slow callback delays everything else in that executor.
A multi-threaded executor runs callbacks on a pool of threads, so independent callbacks can run in parallel. Which callbacks may overlap is controlled by callback groups. The parallelism is not free: shared state now needs protection, and the order of callbacks is less predictable. For a control loop with a strict period I prefer a dedicated real-time thread outside the executor, and use the executor only for the non-critical parts.
What are callback groups, and why can a service call from inside a callback deadlock?Mid
A callback group sets the concurrency rule for the callbacks in it. In a mutually exclusive group, at most one callback runs at a time; in a reentrant group, callbacks may run in parallel, including the same callback more than once. By default every callback is in the node's mutually exclusive default group.
The deadlock: a timer callback sends a service request and then blocks waiting for the response. The response is delivered by the client's callback, which needs the executor. With a single-threaded executor the only thread is the one blocked waiting, so the response is never processed. With a multi-threaded executor the same happens if the client is in the same mutually exclusive group as the timer.
Fixes: send the request asynchronously and handle the result in a callback, without blocking; or put the client in a separate group and use a multi-threaded executor. I prefer the asynchronous style, because blocking in a callback also stalls every other callback in that group.
Explain the main QoS policies, then choose profiles for a lidar scan, a static map and a stop command.Mid
Reliability is best-effort or reliable, meaning whether lost samples are retransmitted. Durability is volatile or transient-local, meaning whether late-joining subscribers receive samples published before they joined. History and depth set how many samples are kept. Deadline sets the expected maximum period between samples, liveliness detects a writer that has gone away, and lifespan discards samples that are too old.
For a lidar scan, best-effort with keep-last of a small depth: a late scan is useless, and retransmission only adds delay. For a static map, reliable and transient-local with depth one, so a planner started later still gets it. For a stop command, reliable with keep-last, plus a deadline so the receiver notices when the sender goes quiet and stops on its own.
Compatibility is checked per pair: a reliable subscriber does not match a best-effort publisher, and a transient-local subscriber does not match a volatile publisher. A real emergency stop should not rely on any of this; it belongs in hardware.
Your 100 Hz control timer in ROS 2 sometimes fires late. Why, and how do you get a steady rate?Mid
A ROS timer is just another callback in the executor. If another callback in the same executor is running, the timer waits until it finishes. Late wake-ups also come from the operating system: a normal-priority thread competes with everything else, and without a real-time kernel and settings the wake-up latency has a long tail. If the node uses simulated time, the timer also depends on how regularly the clock topic arrives.
For a steady rate I take the loop out of the executor. A dedicated thread with a real-time priority sleeps until an absolute deadline with clock_nanosleep on the monotonic clock, then adds the period to the deadline. Using an absolute deadline stops errors from accumulating, which a relative sleep does. The loop reads its inputs from lock-free buffers filled by subscriber callbacks and publishes through a real-time-safe path. I measure the period jitter and record the worst case over long runs, not only the mean.
How does tf2 store transforms, and how does it answer a lookup at a given time?Junior
tf2 keeps a tree of frames in which each frame has exactly one parent. Each edge has a buffer of time-stamped transforms covering a recent window, ten seconds by default. Static transforms, such as sensor mounts, are published once on a separate topic with transient-local durability and kept without a time limit.
For a lookup between two frames at time t, tf2 finds the path through their common ancestor, and for each edge on the path it interpolates between the two stored samples around t: linear interpolation for translation and spherical interpolation for rotation. It then composes the edge transforms. If t lies outside the stored range of any edge, the lookup fails with an extrapolation error rather than guessing. That is why the time of a lookup matters: asking for the transform at the measurement time gives the right answer for a sensor reading, while asking for the latest transform gives a slightly wrong one on a moving robot.
A node logs an error that the lookup would require extrapolation into the future. What does it mean, and what is the right fix?Mid
The node asked for a transform at a time later than the newest sample on some edge of the path. Typically a sensor message has arrived stamped at time t, but the odometry or localisation transform for t has not arrived yet, because it is published at a lower rate or with delay. It can also mean the clocks of two computers disagree, or one node uses simulated time and another does not.
The right fixes wait for the data instead of hiding the error. Look up with a short timeout, or use a message filter that holds sensor messages until the transform for their stamp is available. Check that the transform publishers run fast enough and that clocks are synchronised. The wrong fix, which I would flag in review, is asking for the latest available transform for every lookup: it removes the error message but pairs the sensor data with the wrong robot pose, which smears obstacles when the robot turns.
C++ and Python for robotics software
Robotics code mixes hard timing with heavy maths, so interviewers probe memory, threading and numerical habits as much as language features.
Explain RAII and apply it to a hardware resource such as a serial port to a motor controller.Junior
RAII ties the lifetime of a resource to the lifetime of an object: the constructor acquires it and the destructor releases it, so the release happens on every exit path, including exceptions and early returns.
For a motor controller on a serial port, a class opens and configures the port in its constructor, throwing if it fails, and closes it in its destructor. I would make it non-copyable, because two objects closing the same descriptor is a bug, and movable if ownership needs to be transferred. For a motor, release should also mean safe: the destructor sends a disable command before closing, and the class enables the motor only through an explicit call.
The limit is that destructors do not run if the process is killed or crashes. So the motor controller itself must also stop on a command timeout. RAII covers clean shutdown; the hardware watchdog covers everything else.
Likely follow-up. Why should a destructor not throw, and how do you report a failed disable command?
How do unique and shared pointers interact with intra-process messaging in ROS 2?Mid
When publisher and subscriber are in the same process and intra-process communication is enabled, publishing a unique pointer hands ownership of the message to the middleware. If there is exactly one subscriber that takes ownership, the message can be moved to it with no copy at all. If several subscribers need it, or some take a shared pointer to a const message, the middleware shares one copy where it can and copies only for subscribers that need ownership.
So for large data I publish with a unique pointer and write subscribers that take a const shared pointer when they only read. The trap is publishing by reference or holding on to the message after publishing it: with a reference the library must copy, and using a moved-from unique pointer is a bug. Inter-process subscribers still receive a serialised copy, so a single remote subscriber, such as a recording tool, brings back the serialisation cost.
How do you keep a control loop that uses Eigen and standard containers free of heap allocation?Mid
Allocation in a loop can take the allocator lock or page in fresh memory, which gives unbounded delay. I remove it in several ways. Use fixed-size Eigen types, such as a six-by-six matrix, which live on the stack. For dynamic sizes, allocate once at configuration and reuse, and write expressions so Eigen evaluates into existing storage instead of creating temporaries. Reserve vectors to their maximum size up front, and do not let them grow.
Watch the hidden allocators: std::string building for log messages, std::function with large captures, exceptions, and ROS messages with unbounded sequences. Then prove it rather than hope: run the loop under a test that hooks malloc and fails if it is called after start-up, or use a tool that flags allocations in the real-time thread. Lock memory with mlockall and touch the stack at start-up so page faults do not appear later.
A 1 kHz control thread needs the latest plan from a 10 Hz planning thread. How do you share it?Mid
The control thread only ever wants the newest plan, not a queue of them, and it must never block. A queue would deliver stale plans in order, and a plain mutex could make the control thread wait while the planner copies a large plan.
I would use a triple buffer: three slots, one owned by the writer, one by the reader and one spare, with an atomic index exchanged on each publish and each read. The writer fills its slot, then swaps it with the spare atomically; the reader swaps the spare in if it is newer. Neither side ever waits, and the reader always sees a complete plan. For small data, a sequence lock is simpler: the writer increments a counter before and after writing, and the reader retries if the counter changed or is odd.
What does a PREEMPT_RT kernel give you, and what else must you configure for a real-time loop?Senior
PREEMPT_RT makes almost all of the kernel pre-emptible: interrupt handlers run as threads with priorities, most spinlocks become sleeping locks with priority inheritance, and long non-pre-emptible sections are broken up. The result is a bounded, much shorter worst-case wake-up latency for a high-priority user thread.
The kernel alone is not enough. The loop thread needs a real-time policy, usually SCHED_FIFO, with a priority above the interrupt threads that do not matter to it and below those it depends on, such as the network card for EtherCAT. Memory must be locked and pre-faulted. I isolate a core from the general scheduler, move other interrupts off it, and fix the CPU frequency and disable deep idle states, because waking from them adds latency. Then I measure with a cyclic latency test under heavy load, CPU, network and disk, for hours, and look at the maximum. A system is only real-time with respect to the worst case you have measured and understood.
Which floating-point pitfalls cause real bugs in robotics code?Mid
Several recur. Storing time or position in single precision: a float has about seven significant digits, so a map in a projected coordinate system with values of hundreds of kilometres has only decimetre resolution, and timestamps in seconds since 1970 lose sub-second detail. I use doubles and local origins.
Domain errors in inverse trigonometry: a dot product of unit vectors can come out slightly above one, and acos then returns NaN, so I clamp first. I use atan2 rather than atan of a ratio, to keep the quadrant and avoid division by zero.
NaN propagation: one NaN from a bad sensor reading flows through a filter and poisons every later estimate, so inputs are checked at the boundary. Comparing floats with equality, and subtracting nearly equal numbers, give fragile results. Finally, angle arithmetic must wrap: the difference between 179 and minus 179 degrees is two degrees, not 358.
Frames, transforms and rotations
Most robotics bugs are frame bugs, so interviewers check that you can name, compose and invert transforms without hesitation and pick the right rotation representation.
What is the difference between T_A_B and T_B_A, and how do you avoid frame mistakes in code?Junior
With the convention I use, T_A_B is the pose of frame B expressed in frame A, and it maps a point written in B into A: p_A equals T_A_B times p_B. T_B_A is its inverse. Composition then chains naturally: T_A_C equals T_A_B times T_B_C, with matching inner letters.
To avoid mistakes, I put both frames in every variable name, such as world_T_base or T_world_base, and check that inner frames cancel whenever I multiply. Functions take and return named frame types where the language makes that cheap. I write down the convention in the project, because some libraries name transforms the other way round. And I test with numbers I can check by hand: a sensor mounted half a metre forward of the base, robot rotated ninety degrees, and confirm the result points where a person would expect.
Why does a mobile robot have both a map frame and an odom frame?Junior
They give two different guarantees. The odom frame is continuous: the pose of the base in odom, from wheel odometry and an IMU, changes smoothly with no jumps, but it drifts without bound. The map frame is globally accurate: the pose of the base in map stays consistent with the map over time, but it can jump when localisation corrects itself.
Different consumers need different guarantees. A local controller or obstacle tracker needs smooth motion, so it works in odom; a jump would make an obstacle seem to leap. A global planner and anything referring to places on the map work in map. Because each frame may have only one parent, localisation does not publish map to base directly; it computes map to base, subtracts the current odom to base, and publishes the correction as map to odom. That edge absorbs the jumps and the drift, while odom to base stays smooth.
Compare rotation matrices, Euler angles, axis-angle and quaternions.Mid
A rotation matrix has nine numbers with orthonormality constraints. It composes by multiplication and rotates vectors directly, but drifts from orthonormal after many products and is bulky to store and interpolate.
Euler angles use three numbers and are easy for people to read, so I use them for configuration and display. But there are twelve possible orderings, they have singularities, and adding or interpolating them is not meaningful.
Axis-angle, or a rotation vector, is minimal and natural for small rotations, angular velocity and errors in an estimator, but composing two of them needs a conversion.
Quaternions have four numbers and one unit-norm constraint, no singularity, cheap composition, easy renormalisation and smooth interpolation, so they are the usual choice for storage and computation. Their pitfalls are the double cover, where q and minus q are the same rotation, and the order of components, which differs between libraries.
Likely follow-up. Which representation would you put in an estimator state, and how would you express its error?
What are SO(3) and SE(3), and why do estimators and optimisers work in the tangent space?Senior
SO(3) is the group of 3D rotations and SE(3) the group of rigid transforms, rotation plus translation. Neither is a vector space: you cannot add two rotations, and a rotation parameterised by four or nine numbers has constraints. But near any element the group looks like a vector space of three or six dimensions, its tangent space, with the exponential map going from a small vector to a group element and the logarithm going back.
Estimators use this to express uncertainty and corrections. The state stores the full rotation, while the error and covariance live in the three-dimensional tangent space. An update computes a small correction vector and applies it with the exponential, so the result is always a valid rotation with no need to renormalise and no redundant parameter in the covariance. Optimisers in pose graph SLAM and calibration do the same. The detail to state clearly in an interview is whether the perturbation is applied on the left, in the world frame, or on the right, in the body frame, because Jacobians differ.
How would you calibrate the extrinsic transform between a camera and an IMU, or between a camera and a robot arm?Mid
For a camera on an arm, this is hand-eye calibration. Move the arm to many poses while the camera observes a fixed target; for each pair of poses, the arm motion A and the camera motion B satisfy A X equals X B, where X is the unknown camera-to-flange transform. Solve for rotation first, then translation, then refine all of it with non-linear least squares on reprojection error. Poses must include rotations about different axes, or the problem is degenerate.
For a camera and an IMU, move the rig in front of a target with rich rotation and acceleration, and estimate the extrinsic rotation, translation and time offset together, typically with a continuous-time trajectory fitted to both sensors. The time offset matters as much as the transform.
How do you integrate gyroscope readings into an orientation estimate?Mid
A gyroscope measures angular velocity in the body frame. Over a step dt, the rotation increment is the exponential of omega times dt, which as a quaternion is cos of half the angle as the scalar part and the unit axis times sine of half the angle as the vector part, where the angle is the norm of omega times dt. Because the rate is in the body frame, the increment multiplies on the right: q at k plus one equals q at k times the increment.
Details that matter: subtract the estimated gyro bias first; for small angles avoid dividing by a near-zero norm by using the first-order form; normalise the quaternion now and then; and if the rate changes within the step, use the mean of consecutive samples or a higher-order method. Even then, integrated orientation drifts because of bias and noise, so roll and pitch are corrected with the accelerometer's gravity direction and yaw needs a magnetometer, a heading sensor or visual features.
Kinematics and dynamics
Interviewers want to see that you can derive kinematics for a simple mechanism and understand what Jacobians, singularities and redundancy mean for the robot in motion.
What is the manipulator Jacobian, and what does it tell you?Junior
The Jacobian maps joint velocities to the end-effector twist: the linear and angular velocity of the tool. Each column is the motion the tool gets from moving one joint at unit speed with the others still. For a revolute joint, the angular part of the column is the joint axis and the linear part is the cross product of that axis with the vector from the joint to the tool.
It tells you several things. Its transpose maps a force and torque at the tool to the joint torques that produce it, which is how you do force control and gravity compensation of a payload. Its rank tells you whether the tool can move in every direction: where the rank drops, the arm is at a singularity. Its condition number, or a manipulability measure, tells you how close you are to one, which is useful for choosing poses and for warning before a motion would demand huge joint speeds.
How do you solve inverse kinematics for an arm numerically, and how do you handle singularities?Mid
Iteratively: compute the pose error between the current and target tool pose, expressed as a six-dimensional twist, and solve for the joint step that would remove it using the Jacobian, then repeat until the error is small. Plain pseudo-inverse steps blow up near a singularity because tiny singular values become huge gains.
Damped least squares fixes that: the step is J-transpose times the inverse of J times J-transpose plus lambda squared times the identity, all times the error. The damping trades a little accuracy for bounded joint motion near singularities, and I raise lambda when the manipulability is low. I also clamp the step size, respect joint limits, and restart from several seeds because the problem has multiple solutions and local minima.
For common six-joint arms with a spherical wrist, an analytic solution exists and is faster and returns every solution branch, so I use it where it applies and keep the numerical solver for general chains.
Give a concrete example of a kinematic singularity and what it does to the robot.Mid
On a typical six-joint industrial arm, the wrist singularity occurs when the fourth and sixth joint axes line up, which happens when the fifth joint is at zero. Rotating joint four one way and joint six the other then produces the same tool motion, so one degree of freedom is lost and the arm cannot rotate the tool about one direction.
In practice, a straight-line Cartesian move that passes near this pose asks joints four and six to spin very fast in opposite directions, hitting velocity limits or causing a protective stop.
Remedies: plan in joint space when the exact Cartesian path does not matter; adjust the tool orientation slightly so the path avoids the singular region; use damped least squares so speeds stay bounded at the cost of path accuracy; or use a redundant arm and the extra joint to stay away.
What is kinematic redundancy, and how do you use the null space?Senior
An arm is redundant for a task when it has more joints than the task needs, such as a seven-joint arm placing a tool in six dimensions. There is then a set of joint motions, the null space of the Jacobian, that move the joints without moving the tool.
You can spend that freedom on a secondary objective. The joint velocity is the pseudo-inverse solution for the tool motion, plus the null-space projector, the identity minus J-pseudo-inverse times J, applied to the gradient of a secondary cost. Typical costs: keep joints away from their limits, keep manipulability high, keep the elbow away from an obstacle or a person, or stay close to a comfortable posture. Because of the projection, the secondary task never disturbs the primary one, at least to first order. With several priorities, the same idea is applied recursively, or the problem is set up as a quadratic programme with priorities and inequality constraints, which handles joint limits more cleanly.
Explain the bicycle model of a car-like robot and why it cannot follow any path.Mid
The bicycle model lumps each axle into one wheel. With the rear axle as reference point, the state is x, y and heading, and the inputs are speed v and steering angle delta. Then x-dot is v cos(theta), y-dot is v sin(theta), and theta-dot is v tan(delta) divided by the wheelbase.
The robot is non-holonomic: it cannot move sideways, because the wheels do not slip laterally, and its turning radius has a minimum set by the wheelbase and the steering limit. So a path with a sharp corner or a sideways step cannot be followed exactly, however good the controller. The planner must produce paths of bounded curvature, using primitives such as Dubins curves for forward motion only, or Reeds–Shepp curves when reversing is allowed, or a state lattice or hybrid search that expands only feasible motions.
Write down the rigid-body dynamics of a manipulator and say where they appear in control.Senior
The joint-space equation is M(q) times q-double-dot plus C(q, q-dot) times q-dot plus g(q) equals tau, plus friction and external torques. M is the symmetric positive-definite mass matrix, C collects Coriolis and centrifugal effects, and g is gravity.
It appears in several places. Gravity compensation adds g(q) to the command so a position loop does not have to fight the weight of the arm, which reduces steady-state error and the integral action needed. Computed-torque control cancels the whole model and imposes linear error dynamics, which works well only when the model is accurate. Feedforward for a trajectory computes the torque the desired motion needs, leaving feedback for the error. Collision detection compares measured torque with the model torque and flags a large residual.
How do you compute wheel speeds for a mecanum base from a desired body twist?Mid
For the usual layout, with rollers at 45 degrees forming an X when viewed from above, wheel radius r and half wheelbase and half track lx and ly, the inverse kinematics for body velocities vx, vy and yaw rate omega are:
k = lx + ly
front_left = (vx - vy - k * omega) / r
front_right = (vx + vy + k * omega) / r
rear_left = (vx + vy - k * omega) / r
rear_right = (vx - vy + k * omega) / rI would check the signs on the real robot, because they depend on how the rollers are mounted and on each motor's direction: command pure forward, pure sideways and pure rotation and watch the base. After computing the wheel speeds, if any exceeds the motor limit, I scale all four by the same factor so the direction of motion is kept rather than clipping one wheel. Mecanum wheels slip, especially sideways, so the inverse of this mapping gives poor odometry, and I would fuse an IMU or other source for heading.
Control
Control questions test whether you can make a real actuator follow a command, which means understanding saturation, delay, feedforward and tuning as well as the textbook terms.
How would you tune the position loop of a robot joint on the real hardware?Mid
Start safe: low gains, limited current and speed, the joint away from its end stops, and an emergency stop in reach. If the joint has an inner velocity or current loop, tune that first, because the outer loop assumes it is fast and well damped.
Then raise the proportional gain with small step commands until the response is fast with modest overshoot, add derivative or velocity feedback for damping, and add integral last, only as much as needed to remove offset. Use step sizes small enough not to saturate, or you are tuning the saturation rather than the loop.
Measure rather than eyeball: log command, position, velocity and output at the loop rate. Better still, run a frequency sweep to measure bandwidth and phase margin. Test at several positions and payloads, because inertia and gravity change across the workspace, and use gain scheduling or gravity feedforward if one set of gains does not fit all.
What is feedforward, and why is it important when tracking a trajectory?Mid
Feedforward computes the actuator command from the desired motion and a model, before any error appears. Feedback only acts on error, so a pure feedback controller tracking a moving reference must always lag behind it to produce any command at all.
For a joint following a trajectory, the feedforward terms are typically velocity times a viscous-friction or back-EMF gain, acceleration times inertia, a gravity term from the model and a friction offset in the direction of motion. With good feedforward, the feedback loop only corrects the model error, so tracking error drops sharply and the feedback gains can stay modest, which helps stability and noise.
Two conditions apply. The trajectory must be smooth enough to differentiate, with continuous velocity and bounded acceleration, otherwise feedforward produces spikes. And the model needs identifying on the real machine; a wrong inertia or friction value makes things worse, so I add terms one at a time and check the tracking error each time.
How do the sample rate and computational delay affect a digital controller?Mid
A digital controller sees the plant only at sample instants, and its output is held between samples. That hold behaves like a delay of about half a sample, and computation and communication add more. Delay costs phase at every frequency, and the cost grows with frequency, so it directly reduces the phase margin at the crossover frequency and limits the achievable bandwidth.
A common guideline is to sample ten to twenty times faster than the closed-loop bandwidth. Faster sampling helps up to the point where derivative estimates become noisy, since the change in the measurement per sample becomes comparable to the noise. Inputs must be filtered before sampling to avoid aliasing, and that filter adds its own phase lag.
Jitter matters too: if the period varies, a controller that assumes a fixed dt computes wrong derivatives and integrals. I either keep the period steady with a real-time thread or use the measured dt, and I count the total loop delay, from sensor to actuator, when I analyse stability.
What is model predictive control, and when would you use it rather than PID?Senior
MPC solves an optimisation at every control step: using a model, it predicts the state over a finite horizon, chooses the input sequence that minimises a cost, such as tracking error plus control effort, subject to constraints, applies only the first input, then repeats at the next step with fresh measurements. With a linear model and quadratic cost, each step is a quadratic programme.
I would use it when constraints and preview matter: actuator limits, speed and acceleration limits, obstacle clearances, and a reference known in advance, as with a vehicle following a path through a bend or a legged robot planning foot forces. PID handles one loop well but cannot reason about constraints or anticipate the future.
The costs are a model that must be accurate enough, a solver that must finish within the period every time, and more ways to fail. In practice I warm-start the solver from the previous solution, cap its iterations, check feasibility, and keep a simple fallback controller if the solver fails or overruns.
How would you make a mobile robot follow a planned path?Mid
A good first choice is pure pursuit. Pick a point on the path a lookahead distance ahead, and steer along the circular arc that passes through it; the curvature is two times the lateral offset of that point divided by the lookahead squared. The lookahead is the main tuning knob: short follows tightly but oscillates, long is smooth but cuts corners. Scaling it with speed works well.
For a car-like robot, the Stanley controller combines heading error with cross-track error measured at the front axle, and tends to track more tightly at low speed. For tight spaces or high speeds, MPC handles curvature and acceleration limits explicitly.
Whatever the law, the practical parts matter as much. Slow down before sharp curves and near obstacles, limit acceleration so the wheels do not slip, handle the end of the path so the robot stops at the goal instead of circling it, and detect when the robot has drifted too far from the path and needs a replan.
How do you check that a control loop is stable with enough margin?Senior
For a loop that is close to linear, I measure the open-loop frequency response, injecting a sine sweep or a pseudo-random signal at the controller output and measuring the response, then read the gain margin and phase margin from it. As a rule of thumb I want a phase margin of at least about 45 degrees and a gain margin of at least about 6 dB.
Margins must hold across operating conditions: different arm poses, payloads, temperatures and battery voltages change the plant, so I check the worst cases. Resonances in gearboxes and belts can create a second crossover, which needs a notch filter or lower bandwidth.
For non-linear systems, such as a robot with saturation or a learned policy, linear margins are only part of the picture. I use simulation across the range, Lyapunov arguments where they can be made, and limit checking at run time, so that if behaviour leaves the region where stability was shown, a supervisor takes over.
State estimation and sensor fusion
Interviewers probe whether you can build and debug a filter, which means understanding the assumptions, the noise models and what the innovations tell you.
In what sense is the Kalman filter optimal, and what are the assumptions?Junior
For a linear system with Gaussian process and measurement noise that is white and uncorrelated, and a Gaussian initial state, the Kalman filter computes the exact posterior distribution of the state, which is Gaussian. Its mean is then the minimum mean-squared-error estimate. If the noise is not Gaussian but the system is still linear and the second-order statistics are right, it is still the best linear unbiased estimator.
Each assumption can fail on a robot. Models are non-linear, so we linearise. Noise is often correlated in time, such as a GPS error that wanders slowly, or bias, which must be modelled as an extra state. Some errors are not Gaussian at all, such as wheel slip or a lidar return from the wrong surface, which need outlier gating. And the noise covariances are rarely known exactly. So the filter is optimal for its model, and most of the engineering is making the model close enough.
How would you fuse wheel odometry, an IMU and GNSS for an outdoor robot?Mid
I would run two estimators, matching the odom and map frames. The first fuses wheel odometry and the IMU into a continuous estimate of velocity and pose in the odom frame, with no GNSS, so it never jumps. It uses wheel speeds as velocity measurements and the gyro for yaw rate, with the IMU accelerometer helping roll and pitch on slopes. The second adds GNSS positions to estimate the pose in the map frame and publishes map to odom.
Details matter. GNSS gives positions in latitude and longitude, so I convert to a local metric frame with a fixed origin. The GNSS antenna is not at the base origin, so the lever arm must be included, especially when the robot turns. Heading from GNSS is only observable when moving. Covariances from the receiver are often optimistic, so I inflate them and use innovation gating to reject multipath jumps near buildings. Finally I check that wheel velocity covariance increases when slip is likely, such as during sharp turns or on grass.
How do you handle a measurement that arrives late, after the filter has already moved past its timestamp?Senior
Several options exist, chosen by how late and how often. If the delay is small and constant, the simplest correct method is to keep a short history of states, covariances and inputs. When the late measurement arrives, rewind to the state at its timestamp, apply the update, and re-run the predictions and later updates up to the present. The cost is the re-propagation, which is fine for small states and short delays.
Alternatively, run the filter deliberately behind real time by the maximum expected delay, so all measurements arrive in order, and predict forward from the delayed estimate to produce the current output. This costs latency in the delayed estimate but is simple and exact.
For large states, such as visual-inertial odometry, the filter can keep a clone of the state at the time an image was taken, and apply the update to that clone when processing finishes, which correctly correlates past and present. Whatever the method, I first make sure the timestamps themselves are right, because a wrong stamp hurts more than a late one.
What is innovation gating, and why use the Mahalanobis distance rather than the plain error?Mid
The innovation is the difference between a measurement and the filter's prediction of it. Its covariance S combines the predicted state uncertainty, projected into measurement space, and the measurement noise. Gating rejects a measurement whose innovation is too unlikely under that covariance.
The Mahalanobis distance, y-transpose times S-inverse times y, scales the error by its expected spread in each direction, so it accounts for the fact that a one-metre error may be normal along a corridor but impossible across it. For a correct filter it follows a chi-squared distribution with as many degrees of freedom as the measurement has dimensions, so the threshold comes from a table: about 9.21 for two dimensions at 99 per cent.
The danger is a filter that has become overconfident: its S is too small, it rejects every correct measurement, and it never recovers. So I count consecutive rejections and, beyond a limit, inflate the covariance or reinitialise, and I log rejection rates as a health metric.
What is observability in state estimation? Give examples of states that are not observable.Senior
A state is observable if the measurements over time, together with the inputs, determine it uniquely. If not, the filter cannot correct it, its uncertainty should grow, and a linearised filter that wrongly treats it as observable becomes overconfident and inconsistent.
Examples. In visual-inertial odometry, global position and the yaw about gravity are unobservable, because rotating or shifting the whole trajectory changes no measurement; only roll and pitch are pinned by gravity. In monocular vision alone, absolute scale is unobservable. IMU biases are observable only with enough motion: an accelerometer bias cannot be separated from tilt while the robot sits still. Heading from GNSS positions alone is unobservable when the robot is stationary. A wheel-odometry-only robot cannot observe its absolute pose at all.
Practical consequences: do not let a filter shrink the covariance of an unobservable state through linearisation errors, use first-estimate Jacobians or a formulation that preserves the correct unobservable directions, and plan motion to excite the states you need, as calibration routines do.
What is weight degeneracy in a particle filter, and how do you manage it?Mid
After a few updates, most particles end up with negligible weight and one or a few carry almost all of it. The filter then represents the belief with very few effective samples and wastes computation on the rest. The effective sample size, one over the sum of squared normalised weights, measures this.
Resampling fixes it by drawing a new set in proportion to the weights. I resample only when the effective sample size drops below about half the particle count, because resampling every step throws away diversity. Systematic, low-variance resampling is preferred: it is linear in time and adds less randomness than independent draws.
Resampling causes the opposite problem, impoverishment: many identical copies of a few particles. Process noise in the motion model spreads them out again, and if the sensor model is very sharp, I widen it a little so that small pose errors do not zero the weights. To recover from a wrong convergence, such as a kidnapped robot, a small fraction of random particles is injected when the measurement likelihood drops.
SLAM, localisation and perception
These questions check that you understand how robots build maps, place themselves in them and turn raw camera and lidar data into geometry they can trust.
Explain the SLAM problem and compare filter-based with graph-based approaches.Mid
SLAM estimates the robot trajectory and a map at the same time, from sensor data and odometry, when neither is known in advance. It is hard because pose errors corrupt the map and map errors corrupt the pose, and because data association, deciding which observation matches which part of the map, is uncertain.
Filter-based SLAM, such as an EKF over the robot pose and landmark positions, keeps only the current pose and marginalises out the past. The covariance becomes dense, the cost grows quadratically with landmarks, and linearisation errors become permanent once past poses are gone.
Graph-based SLAM keeps the trajectory as a graph of poses, with edges from odometry, scan matching and loop closures, and optimises the whole graph as non-linear least squares. The problem is sparse, so it scales well, and relinearising past poses keeps it accurate.
What does a pose graph contain, and how is it optimised?Senior
Nodes are robot poses at selected keyframes. Each edge is a measured relative transform between two poses, with an information matrix, the inverse covariance, saying how much to trust it: odometry edges between consecutive poses, and loop-closure edges between a pose and an earlier one when the robot recognises a place.
Optimisation minimises the sum of squared errors, each the difference between the measured relative transform and the one implied by the current pose estimates, weighted by the information matrix. It is non-linear because of rotations, so it uses Gauss–Newton or Levenberg–Marquardt with updates in the tangent space. Each iteration solves a sparse linear system, fast with sparse Cholesky factorisation and a good variable ordering. One pose must be fixed, because the whole graph could otherwise move freely.
The practical risk is a false loop closure, which bends the whole map. Loss functions that grow slowly for large errors, such as Huber or Cauchy, reduce the pull of large residuals, and switchable constraints or consistency checks between closures reject the bad ones.
Explain the pinhole camera model and what intrinsic calibration estimates.Junior
A point at X, Y, Z in the camera frame, with Z along the optical axis, projects to normalised coordinates X over Z and Y over Z. The intrinsic matrix maps those to pixels: u equals fx times x plus cx, v equals fy times y plus cy, where fx and fy are focal lengths in pixels and cx and cy the principal point. Real lenses add distortion, usually modelled with radial terms and two tangential terms, or with a fisheye model for wide lenses.
Intrinsic calibration estimates these parameters from many images of a known target, such as a checkerboard, seen at different positions, distances and tilts that cover the whole image, especially the corners where distortion is largest. The solver minimises reprojection error over intrinsics and the target poses. I judge the result by the reprojection error, which should be a fraction of a pixel, by its uniformity across the image, and by checking it on images not used in the fit. A calibration is tied to focus and zoom settings, so those must be locked.
How does ICP work, and when does it fail?Mid
Iterative closest point aligns a source point cloud to a target. Each iteration pairs every source point with its nearest target point, rejects pairs that are too far apart, computes the rigid transform that best aligns the pairs in a least-squares sense, applies it, and repeats until the change is small. Point-to-plane ICP minimises distance along the target surface normal instead of point distance, which converges much faster on structured scenes because points can slide along surfaces.
It fails in predictable ways. It is a local method: with a poor initial guess, nearest neighbours are wrong and it converges to a wrong alignment, so it needs odometry or a global method to start from. In geometrically degenerate scenes, such as a long featureless corridor or a flat open field, the alignment along the corridor or in the plane is unconstrained and drifts. Moving objects and partial overlap pull it off. I check the condition of the final system, the inlier ratio and the residual, and report a covariance so the fusion layer can weight it.
How would you process a 3D lidar point cloud to detect obstacles for a mobile robot?Mid
A typical pipeline runs within the sensor period. First, correct motion distortion, then transform into the base frame at the scan time. Crop to the region of interest and remove points on the robot body. Downsample with a voxel grid to bound the cost of later steps.
Next, separate the ground. On flat floors, fitting a plane with RANSAC works; on uneven terrain, a local method, such as comparing the slope between consecutive points in each laser ring or fitting small patches, avoids removing ramps or keeping kerbs. Then cluster the non-ground points by Euclidean distance, using a kd-tree or the sensor's own range image to find neighbours fast, and discard clusters too small to be real.
Each cluster becomes an obstacle with a bounding shape and, when tracked over time, a velocity. I watch for dust, rain and glass, which produce false or missing returns, and I test on recorded data from those conditions.
How do you evaluate a localisation or SLAM system?Senior
With ground truth from motion capture, a survey-grade reference or a carefully built map, I compute two error measures. Absolute trajectory error aligns the estimated trajectory with ground truth, using a rigid transform or also scale for monocular systems, and reports the root mean square of position differences, which measures global consistency. Relative pose error compares motion over fixed intervals of distance or time, which measures drift and is fairer for odometry.
Numbers on public datasets are a start, but product questions decide. How often does localisation fail or need a human, per hour or per kilometre? How quickly does it recover? How does accuracy change with crowds, rearranged furniture, lighting or season? What are the CPU and memory costs on the target computer? I build a regression suite of recorded runs from real sites, including the difficult ones, run it on every change and track these metrics over time, so a change that improves one site but breaks another is caught before release.
Motion planning
Planning questions test whether you can choose the right planner for the robot's dimensions and constraints, and turn a path into motion the hardware can actually execute.
What is configuration space, and why do planners work in it?Junior
The configuration space has one dimension for each degree of freedom: x, y and heading for a mobile base, or the joint angles for an arm. A single point in it describes the whole robot. Obstacles in the workspace map to forbidden regions in configuration space, the configurations where any part of the robot would collide.
Planning there turns a problem about a shaped body moving among shapes into a problem about a point moving among regions, which is what search and sampling algorithms need. For a circular mobile robot, the configuration-space obstacle is the workspace obstacle inflated by the robot radius, which is exactly what costmap inflation does. For an arm, the configuration-space obstacles have no simple closed form, so planners test configurations one at a time with a collision checker. That is why sampling-based planners dominate for arms, and why collision checking is usually the main cost of planning.
What makes an A* heuristic admissible and consistent, and what happens if it is neither?Junior
An admissible heuristic never overestimates the true cost to the goal. A consistent one also satisfies the triangle inequality: the estimate at a node is no more than the edge cost to a neighbour plus the estimate at that neighbour. Consistency implies admissibility.
With an admissible heuristic, A* returns an optimal path. With a consistent one, each node is expanded at most once, so a closed set can be used without reopening nodes. On an eight-connected grid with diagonal cost of root two, the octile distance is consistent; Euclidean distance is also consistent but expands more nodes because it is weaker.
An overestimating heuristic can return a suboptimal path, but it often expands far fewer nodes. Weighted A*, multiplying the heuristic by a factor greater than one, uses this deliberately and guarantees the cost is at most that factor times the optimum. That is a sensible trade-off on a large map where speed matters more than the last few per cent of path length.
How does RRT work, and what do RRT* and RRT-Connect add?Mid
RRT grows a tree from the start. Each iteration samples a random configuration, often biased to the goal a small fraction of the time, finds the nearest tree node, and steers a short step from it towards the sample. If the step is collision-free, it becomes a new node. When a node gets close enough to the goal, the path is read back through the parents. The random sampling pulls the tree into unexplored space quickly, which makes it effective in high dimensions.
RRT finds a feasible path but not a good one, and the path quality does not improve with time. RRT* adds two steps: a new node chooses the best parent among nearby nodes, and nearby nodes are rewired through the new node when that shortens their path. The solution then converges towards the optimum as samples increase, at a higher cost per iteration.
RRT-Connect grows trees from both the start and the goal and repeatedly tries to connect them greedily, which is very fast for single queries with arms. In all cases I follow with shortcutting and smoothing.
How do you turn a geometric path into a trajectory a robot can execute?Mid
A path is a sequence of configurations; a trajectory adds timing, giving position, velocity and acceleration at each instant. Two steps are usually needed. First, make the path smooth enough, since the corners of a piecewise-linear path demand infinite acceleration if followed at speed. Options are blending corners with arcs or splines, or fitting a spline through the waypoints while checking the result is still collision-free.
Second, time-parameterise it within the limits. For a single axis, a trapezoidal velocity profile respects velocity and acceleration limits; an S-curve also limits jerk, which reduces vibration and wear. For a path through many joints, time-optimal path parameterisation finds the fastest speed along the path at each point that keeps every joint within its velocity and acceleration limits, by treating the speed along the path as the single variable.
What is trajectory optimisation, and how does it compare with sampling-based planning?Senior
Trajectory optimisation represents the whole trajectory as variables, such as waypoints or spline coefficients, and minimises a cost, typically smoothness plus a collision penalty from a signed distance field, subject to constraints such as joint limits, dynamics and the start and goal. It uses gradient-based solvers, often sequential convex or quadratic programmes.
Its strengths are smooth, high-quality motions, natural handling of costs like distance from people or effort, and constraints on dynamics. Its weakness is that it is local: it can get stuck in a minimum where the trajectory passes through an obstacle, or fail to find the corridor on the other side of an obstacle, so it depends on its initial guess and gives no completeness guarantee.
Sampling-based planners are the opposite: they find a feasible path in cluttered, high-dimensional spaces, but the path is rough. The common combination is to use a sampling planner for a feasible path and initialise the optimiser from it, or to run the optimiser from several seeds and keep the best result.
How should a mobile robot's local planner deal with people walking around it?Senior
Static obstacle avoidance is not enough, because a person who is two metres away and walking towards the robot is closer in time than a wall one metre to the side. I would track people over time to estimate their velocity, predict their motion over a short horizon, and score candidate motions against those predictions, for example with time to collision or velocity obstacles, rather than against the current snapshot only.
Behaviour matters as much as geometry. The robot should slow down near people, keep a larger margin than for objects, avoid cutting directly in front of someone, and make its intent readable by moving smoothly instead of hesitating. Predictions are uncertain, so I prefer motions that leave options open and stop when uncertainty is high.
The safety layer stays separate. Speed and separation monitoring with a safety-rated sensor limits speed by distance regardless of what the planner decides. And I test with recorded crowds and scripted scenarios in simulation, including people who stop suddenly or step out from behind shelves.
Testing, simulation, safety and deployment
Senior rounds focus here: how you know the robot works, how you keep people safe when it does not, and how software reaches many robots without stopping a customer's operation.
How do you structure the tests for a robotics codebase?Junior
As a pyramid. At the base, many fast unit tests for pure code: geometry, kinematics, filter steps and planners, with hand-checked cases, property tests such as transforming and then inverse-transforming a point, and edge cases such as angles at plus and minus pi. These run in seconds on every commit, without any middleware.
Above that, integration tests that launch a few nodes and check their interaction: topics appear, messages flow, a service returns the right answer, and a node handles missing inputs. Then simulation tests that run whole behaviours, such as docking or picking, across many scenarios and record pass rates. Then log replay tests, which feed recorded real data through perception and estimation and compare with stored results. Hardware-in-the-loop tests run the real controller against simulated or bench hardware, and field tests are the final, most expensive layer.
What are the limits of simulation, and how do you manage the gap between simulation and the real robot?Mid
Simulation is weak at contact, friction, deformable objects, sensor noise and failure modes, and at timing: a simulator usually runs everything synchronously and on time, while the real robot has latency, jitter and dropped messages. A policy or controller tuned only in simulation can exploit simulator errors.
To manage the gap, I identify key parameters on the real robot, such as masses, friction, motor limits and sensor delays, and put them into the simulator. I add realistic noise, latency and drop-outs to simulated sensors. I randomise uncertain parameters across runs so the software works over a range rather than at one point. I keep the same launch files and software on both, with only the hardware interface swapped. Most importantly, I compare simulation and reality on the same scenario regularly, with recorded data, and treat a growing difference as a bug. Simulation decides what is worth trying on the robot; it does not replace that trial.
How do you make robot software deterministic enough to test reliably?Senior
Flaky tests in robotics usually come from time, threads and randomness. I remove each source deliberately. Algorithms take time as an input rather than reading the system clock, so a test can supply exact times. Randomised algorithms take a seeded generator as a parameter. Core algorithms are written as functions of their inputs, so a unit test does not depend on executor scheduling.
For integration tests, the simulator drives a simulated clock and steps physics and nodes in lock-step, so the order of events is repeatable. Tests wait on conditions with timeouts, not on fixed sleeps. Where a race is possible, such as a subscriber that might connect after the first message, the test either uses transient-local durability or waits for the connection before publishing.
When a test is still flaky, I treat it as a real bug: often it is a race that will also happen on the robot.
What do functional safety standards expect from a robot software team?Senior
The general standard for programmable safety systems is IEC 61508, and machinery safety commonly uses ISO 13849 with its performance levels or IEC 62061. Robot-specific standards build on these, such as ISO 10218 for industrial robots and ISO 3691-4 for driverless industrial trucks.
They ask for a hazard analysis and risk assessment first, from which safety functions and their required integrity are derived. Each safety requirement is traceable to design, code and tests. Safety functions are implemented with appropriate independence, often in certified safety controllers and safety-rated sensors rather than in the main robot computer. Development follows a defined process with reviews, coding rules, coverage targets and controlled changes.
For most robot software engineers, the practical effect is knowing which functions are safety functions, keeping the main software out of them, and not weakening them, for example by changing a speed limit in a configuration file without going through the safety process.
How do you roll out a new software version to a fleet of robots without disrupting customers?Senior
Updates must be atomic and reversible. I build the whole robot software as one versioned image or set of containers, with the configuration and maps versioned separately. On the robot, an A/B scheme installs the new version beside the old one and switches only after a health check passes, rolling back automatically if the robot fails to come up healthy.
Rollout is staged. First internal test robots, then a small canary group of customer robots, then larger waves, with a pause and a review of metrics between waves: mission success rate, interventions, localisation failures, error rates and loop timing. A regression stops the rollout.
Timing respects operations: robots download in the background and install only when idle, docked and charging, never mid-mission, and a site can defer an update during a busy period. I also keep compatibility between versions where robots talk to each other or to a fleet server, because a mixed fleet is normal during a rollout.
A customer reports that a robot occasionally stops in one aisle, and you cannot reproduce it in the office. How do you approach it?Mid
First gather facts without guessing: which robots, which software version and map, which aisle, which times of day, and what the robot reported when it stopped. If the black-box recorder captured the event, I pull the recorded sensor data and logs from just before the stop.
Then form hypotheses that fit the pattern. A stop in one aisle suggests something about that place: reflective surfaces confusing the lidar, a sloped floor triggering a tilt check, a narrow gap close to the inflation radius, or a dead spot in Wi-Fi where the robot loses its fleet server.
I test hypotheses by replaying the recording through the same software version, which often reproduces the decision exactly. If the data is missing, I add targeted logging or a trigger for that area and wait for the next event. Once the cause is found, the fix comes with a regression test built from the recorded data and a check across the fleet for the same signature at other sites.
Coding exercises
Set a timer for 30 minutes per exercise. Write the code in an editor, compile it, and test the edge cases before you open the solution.
- Differential-drive odometry from encoder ticksEasy · Python
A differential-drive robot reports the change in encoder ticks for each wheel every control cycle. Write a function that updates the pose (x, y, theta) given the tick deltas, ticks per wheel revolution, wheel radius and track width. Keep theta in the range from minus pi to pi. The encoders are 16-bit counters that wrap, so also write a helper that turns two raw counter readings into a signed delta.
Example: with 1000 ticks per revolution, a 0.05 m wheel radius and both wheels advancing 10 ticks for 100 cycles from the origin, the robot ends at x of about 0.314 m, y of 0 and theta of 0.
Hints
- Convert ticks to distance for each wheel first; the centre moves by their mean and the heading changes by their difference over the track width.
- Use the heading at the middle of the interval when projecting the distance onto x and y.
- For the counter, take the difference modulo 2 to the power 16 and map large values to negative ones.
Solution and explanation
import math def wrap(a): """Wrap an angle to the interval [-pi, pi).""" return (a + math.pi) % (2.0 * math.pi) - math.pi def odom_update(pose, d_left_ticks, d_right_ticks, ticks_per_rev, wheel_radius, track_width): """Advance (x, y, theta) by one encoder interval using mid-point heading.""" x, y, th = pose per_tick = 2.0 * math.pi * wheel_radius / ticks_per_rev dl = d_left_ticks * per_tick dr = d_right_ticks * per_tick ds = 0.5 * (dl + dr) # distance travelled by the centre dth = (dr - dl) / track_width # heading change, left turn positive th_mid = th + 0.5 * dth x += ds * math.cos(th_mid) y += ds * math.sin(th_mid) return (x, y, wrap(th + dth)) def ticks_delta(now, prev, bits=16): """Signed difference of a free-running counter that wraps at 2**bits.""" m = 1 << bits d = (now - prev) % m return d - m if d >= m // 2 else dEach wheel's distance is its ticks times the circumference over ticks per revolution. The axle centre moves by the mean of the two, and the heading changes by their difference over the track width, positive for a left turn. Projecting with the mid-point heading is accurate to second order for a constant-curvature arc, and clearly better than the starting heading when turning at speed.
The wrap helper is what interviewers listen for. A 16-bit counter going from 65530 to 5 has moved 11 ticks forward, not 65525 back. Taking the difference modulo the range and treating the upper half as negative handles both directions, provided the wheel moves less than half the range per cycle, which constrains the polling rate. Each update is constant time. Mention that the model assumes no slip, that track width is best calibrated by spinning on the spot, and that the error grows without bound, so this output feeds a filter.
- 2D rigid transforms: compose, invert and wrap anglesEasy · C++
Implement a small SE(2) type in C++ for a mobile robot. It must apply a transform to a point, compose two transforms, invert a transform, and give the smallest signed difference between two headings. Use the convention that T_A_B maps points from frame B to frame A.
Example: the base is at (2, 1) facing +y in the world, and a camera is 0.5 m forward of the base. The camera in the world is then at (2, 1.5), and a point 1 m in front of the camera is at (2, 2.5). Composing a transform with its inverse must give the identity.
Hints
- Composition applies the first transform's rotation to the second transform's translation, then adds.
- The inverse translation is minus R-transpose times t.
Solution and explanation
#include <cmath> constexpr double kPi = 3.14159265358979323846; struct Vec2 { double x, y; }; // Pose of frame B expressed in frame A: T_A_B. Maps points from B to A. struct SE2 { double x = 0.0, y = 0.0, th = 0.0; static double wrap(double a) { a = std::fmod(a + kPi, 2.0 * kPi); if (a < 0.0) a += 2.0 * kPi; return a - kPi; } Vec2 apply(const Vec2& p) const { // p_A = T_A_B * p_B const double c = std::cos(th), s = std::sin(th); return {c * p.x - s * p.y + x, s * p.x + c * p.y + y}; } SE2 compose(const SE2& b) const { // T_A_C = T_A_B * T_B_C const Vec2 t = apply({b.x, b.y}); return {t.x, t.y, wrap(th + b.th)}; } SE2 inverse() const { // T_B_A const double c = std::cos(th), s = std::sin(th); return {-(c * x + s * y), -(-s * x + c * y), wrap(-th)}; } }; // Smallest signed rotation that takes heading a to heading b. inline double angle_diff(double b, double a) { return SE2::wrap(b - a); }Applying the transform rotates the point and adds the translation. Composing T_A_B with T_B_C applies T_A_B to the origin of C to get the new translation and adds the angles. The inverse rotates the negated translation by minus theta, which is minus R-transpose times t written out. Every operation is constant time.
The wrap uses fmod and corrects negative results, because fmod keeps the sign of its first argument. With it, the difference from 3.0 to minus 3.0 radians is about 0.28, not minus 6. This function sits in every heading controller and every yaw innovation, and getting it wrong makes a robot turn the long way round. Pi is defined as a constant because M_PI is not standard C++. State the convention before writing code, and test with a ninety-degree case you can check by hand.
- PID controller with anti-windup and derivative filteringEasy · C++
Write a PID controller class for a joint or motor speed loop. Requirements: output limits, no integrator wind-up when the output saturates, no derivative kick when the setpoint steps, a first-order low-pass filter on the derivative, and correct behaviour on the first call. Use no heap allocation in update.
Test: on a first-order plant with actuator limits of plus and minus one and a setpoint of 1.5, the output settles with negligible overshoot, and with an unreachable setpoint the integral stays bounded.
Hints
- Differentiate the measurement, not the error, and skip the derivative on the first call.
- Compute the candidate integral, then only keep it if the output is not saturated in the direction the error is pushing.
Solution and explanation
#include <algorithm> struct PidConfig { double kp, ki, kd; double out_min, out_max; // actuator limits double d_filter_tau; // seconds, low-pass on the derivative term }; class Pid { public: explicit Pid(const PidConfig& c) : c_(c) {} void reset() { integ_ = 0.0; d_ = 0.0; first_ = true; } // dt in seconds; returns the saturated command. double update(double setpoint, double measured, double dt) { if (dt <= 0.0) return last_out_; const double err = setpoint - measured; // Derivative on measurement: no kick when the setpoint steps. double d_raw = first_ ? 0.0 : -(measured - prev_meas_) / dt; const double a = dt / (c_.d_filter_tau + dt); d_ += a * (d_raw - d_); prev_meas_ = measured; first_ = false; const double p_term = c_.kp * err; const double d_term = c_.kd * d_; const double candidate_i = integ_ + c_.ki * err * dt; const double unsat = p_term + candidate_i + d_term; const double out = std::clamp(unsat, c_.out_min, c_.out_max); // Conditional integration: only accept the new integral if it does // not push further into saturation. const bool pushing_high = unsat > c_.out_max && err > 0.0; const bool pushing_low = unsat < c_.out_min && err < 0.0; if (!pushing_high && !pushing_low) integ_ = candidate_i; last_out_ = out; return out; } double integral() const { return integ_; } private: PidConfig c_; double integ_ = 0.0, d_ = 0.0, prev_meas_ = 0.0, last_out_ = 0.0; bool first_ = true; };The derivative uses the negative rate of change of the measurement, which equals the error rate while the setpoint is constant but ignores setpoint steps. A first-order low-pass smooths it, since differentiating a quantised encoder signal is mostly noise, and the first call returns no derivative rather than a spike.
Anti-windup is conditional integration: the candidate integral is kept only if the output is not saturated in the direction the error is pushing. When the error changes sign, integration resumes at once, so the controller recovers without the overshoot wind-up causes. Back-calculation is an alternative with smoother behaviour at the limits.
The class holds only scalars, so update is allocation-free and constant time. Interviewers also listen for handling of a zero or negative dt, a reset for mode changes, and applying ki inside the integral so changing it at run time does not make the output jump.
- Quaternion rotation and slerpMedium · Python
Using only the standard library, implement quaternion multiplication, conjugate, construction from an axis and angle, rotation of a 3D vector, spherical linear interpolation, and extraction of yaw. Use (w, x, y, z) order and the Hamilton convention.
Requirements: slerp must take the shorter path, so interpolating halfway between a yaw of 170 degrees and a yaw of minus 170 degrees gives 180 degrees, not 0. It must also handle two nearly identical inputs without dividing by zero.
Hints
- If the dot product of the two quaternions is negative, negate one of them before interpolating.
- When the angle between them is tiny, fall back to linear interpolation followed by normalisation.
Solution and explanation
import math # Quaternions as (w, x, y, z), Hamilton convention, unit length for rotations. def q_mul(a, b): aw, ax, ay, az = a bw, bx, by, bz = b return (aw * bw - ax * bx - ay * by - az * bz, aw * bx + ax * bw + ay * bz - az * by, aw * by - ax * bz + ay * bw + az * bx, aw * bz + ax * by - ay * bx + az * bw) def q_conj(q): return (q[0], -q[1], -q[2], -q[3]) def q_normalise(q): n = math.sqrt(sum(c * c for c in q)) return tuple(c / n for c in q) def q_from_axis_angle(axis, angle): n = math.sqrt(sum(c * c for c in axis)) s = math.sin(angle / 2.0) / n return (math.cos(angle / 2.0), axis[0] * s, axis[1] * s, axis[2] * s) def q_rotate(q, v): """Rotate vector v by unit quaternion q: q * (0, v) * conj(q).""" r = q_mul(q_mul(q, (0.0, v[0], v[1], v[2])), q_conj(q)) return (r[1], r[2], r[3]) def q_slerp(a, b, t): dot = sum(x * y for x, y in zip(a, b)) if dot < 0.0: # q and -q are the same rotation: b, dot = tuple(-c for c in b), -dot # take the short way round if dot > 0.9995: # nearly parallel: lerp avoids 0/0 return q_normalise(tuple(x + t * (y - x) for x, y in zip(a, b))) theta = math.acos(dot) s = math.sin(theta) wa = math.sin((1.0 - t) * theta) / s wb = math.sin(t * theta) / s return tuple(wa * x + wb * y for x, y in zip(a, b)) def q_yaw(q): """Heading (rotation about z) of a unit quaternion, ZYX convention.""" w, x, y, z = q return math.atan2(2.0 * (w * z + x * y), 1.0 - 2.0 * (y * y + z * z))Rotating a vector embeds it as a pure quaternion and computes q times v times the conjugate, which is the inverse for a unit quaternion. With this convention, q_mul(a, b) rotates by b first and then by a, matching rotation matrix order.
Slerp moves at constant angular speed along the great arc. Because q and minus q are the same rotation, the arc can go the long way, which in the yaw example would sweep through zero instead of 180 degrees; a negative dot product means one input must be negated. When the dot product is near one, the sine of the angle is near zero and the formula is unstable, so normalised linear interpolation takes over with negligible error.
Yaw extraction is well defined except as pitch approaches ninety degrees, which is gimbal lock in the Euler angles, not a quaternion problem. Each operation is constant time.
- A* on an occupancy grid with diagonal movesMedium · Python
Given a 2D occupancy grid where 1 marks an obstacle, find the shortest path from start to goal with eight-connected moves: straight moves cost 1 and diagonal moves cost the square root of two. A diagonal move may not cut the corner of an obstacle. Return the cost and the list of cells, or infinity and an empty list if no path exists.
Discuss your heuristic and prove to the interviewer that the result is optimal.
Hints
- The octile distance is the exact cost on an empty eight-connected grid, so it is admissible and consistent.
- Use a heap with lazy deletion: push duplicates and skip entries for nodes already closed.
Solution and explanation
import heapq import math SQRT2 = math.sqrt(2.0) MOVES = [(1, 0, 1.0), (-1, 0, 1.0), (0, 1, 1.0), (0, -1, 1.0), (1, 1, SQRT2), (1, -1, SQRT2), (-1, 1, SQRT2), (-1, -1, SQRT2)] def octile(a, b): dx, dy = abs(a[0] - b[0]), abs(a[1] - b[1]) return (dx + dy) + (SQRT2 - 2.0) * min(dx, dy) def astar(grid, start, goal): """grid[r][c] == 1 is an obstacle. Cells are (r, c). Returns (cost, path) or (inf, []) when the goal cannot be reached. No corner cutting.""" rows, cols = len(grid), len(grid[0]) def free(r, c): return 0 <= r < rows and 0 <= c < cols and grid[r][c] == 0 if not free(*start) or not free(*goal): return math.inf, [] g = {start: 0.0} parent = {start: None} open_heap = [(octile(start, goal), 0.0, start)] closed = set() while open_heap: _, gc, cur = heapq.heappop(open_heap) if cur in closed: continue # stale entry: lazy deletion if cur == goal: path = [] while cur is not None: path.append(cur) cur = parent[cur] return gc, path[::-1] closed.add(cur) r, c = cur for dr, dc, step in MOVES: nr, nc = r + dr, c + dc if not free(nr, nc): continue if dr and dc and not (free(r + dr, c) and free(r, c + dc)): continue # do not squeeze past a corner ng = gc + step nxt = (nr, nc) if ng < g.get(nxt, math.inf): g[nxt] = ng parent[nxt] = cur heapq.heappush(open_heap, (ng + octile(nxt, goal), ng, nxt)) return math.inf, []A* expands nodes in order of g plus h. The octile distance is the exact cost on an empty grid, so it never overestimates and it is consistent. With a consistent heuristic, a node's g value is final when it is first popped, so the closed set is safe and the path is optimal when the goal is popped. The test harness checks this against Dijkstra on hundreds of random grids.
Lazy deletion replaces decrease-key, which Python's heap lacks: improved entries are pushed again and stale ones skipped. With N free cells, time is O(N log N) and memory O(N).
The corner rule blocks a diagonal move if either cell it passes between is occupied, since a real robot would clip it. Cover start or goal blocked, start equal to goal and no path. Common follow-ups: inflate obstacles by the robot radius, add costmap costs, and break ties to reduce expansions.
- Low-variance resampling for a particle filterMedium · Python
Implement the resampling step of a particle filter. Given particles and their unnormalised weights, return n new particles drawn in proportion to the weights, each with weight 1/n. It must run in O(n) and keep as much diversity as possible. Also write the effective sample size, and a wrapper that resamples only when the effective sample size falls below half the particle count.
Hints
- Use one random offset in the interval from 0 to total weight over n, then step through the cumulative weights with n evenly spaced pointers.
- The pointers only move forward, so one pass over the particles is enough.
Solution and explanation
import random def effective_sample_size(weights): s = sum(weights) return 1.0 / sum((w / s) ** 2 for w in weights) def low_variance_resample(particles, weights, rng=random): """Systematic resampling: one random offset, n evenly spaced pointers. O(n), and a particle with normalised weight w is copied floor(n*w) or ceil(n*w) times. Returns new particles with uniform weights.""" n = len(particles) total = sum(weights) if n == 0 or total <= 0.0: raise ValueError("need at least one particle with positive weight") step = total / n u = rng.uniform(0.0, step) out = [] i = 0 cumulative = weights[0] for _ in range(n): while u >= cumulative and i < n - 1: # guard against rounding at the end i += 1 cumulative += weights[i] out.append(particles[i]) u += step return out, [1.0 / n] * n def maybe_resample(particles, weights, threshold=0.5, rng=random): """Resample only when the weights have degenerated.""" if effective_sample_size(weights) < threshold * len(particles): return low_variance_resample(particles, weights, rng) s = sum(weights) return particles, [w / s for w in weights]Systematic resampling places n pointers spaced by the mean weight from one random offset and walks the cumulative weight once. A particle with weight share w is chosen floor(n w) or ceil(n w) times, the smallest possible spread, so it adds far less randomness than n independent draws. The harness checks that property on thousands of random weight vectors. Both indices only move forward, so the cost is O(n).
The greater-or-equal comparison means a zero-weight particle is never picked, even if the offset is exactly zero, and the index guard absorbs rounding at the end of the cumulative sum. The wrapper resamples only when the effective sample size falls below half the count, because resampling every step destroys diversity. Afterwards, motion noise spreads the copies out again.
- Two-link planar arm: forward and inverse kinematicsMedium · Python
A planar arm has two revolute joints with link lengths l1 and l2. Write forward kinematics, an analytic inverse kinematics function that returns every solution for a target (x, y), the Jacobian, and a manipulability measure. Return no solutions for an unreachable target and one solution on the boundary of the workspace.
Hints
- The law of cosines gives the cosine of the elbow angle from the distance to the target.
- Use atan2 for every angle; the sign of the sine of the elbow angle picks elbow-up or elbow-down.
Solution and explanation
import math def fk(l1, l2, q1, q2): """End-effector position of a planar two-link arm.""" x = l1 * math.cos(q1) + l2 * math.cos(q1 + q2) y = l1 * math.sin(q1) + l2 * math.sin(q1 + q2) return x, y def ik(l1, l2, x, y, eps=1e-9): """All joint solutions (q1, q2) that reach (x, y). Returns [] if the target is out of reach; one solution at the workspace boundary.""" r2 = x * x + y * y c2 = (r2 - l1 * l1 - l2 * l2) / (2.0 * l1 * l2) if c2 > 1.0 + eps or c2 < -1.0 - eps: return [] c2 = max(-1.0, min(1.0, c2)) # clamp rounding noise before sqrt s2_abs = math.sqrt(1.0 - c2 * c2) sols = [] for s2 in ((s2_abs, -s2_abs) if s2_abs > eps else (0.0,)): q2 = math.atan2(s2, c2) k1 = l1 + l2 * c2 k2 = l2 * s2 q1 = math.atan2(y, x) - math.atan2(k2, k1) q1 = (q1 + math.pi) % (2.0 * math.pi) - math.pi sols.append((q1, q2)) return sols def jacobian(l1, l2, q1, q2): s1, c1 = math.sin(q1), math.cos(q1) s12, c12 = math.sin(q1 + q2), math.cos(q1 + q2) return [[-l1 * s1 - l2 * s12, -l2 * s12], [l1 * c1 + l2 * c12, l2 * c12]] def manipulability(l1, l2, q2): """|det J| = l1 * l2 * |sin q2|; zero when the arm is straight or folded.""" return abs(l1 * l2 * math.sin(q2))The law of cosines gives cos(q2) from the squared distance to the target; outside the range minus one to one the target is unreachable. The sine is plus or minus the square root of one minus the cosine squared, giving elbow-down and elbow-up, which coincide when the arm is straight or folded. The shoulder angle is the target bearing minus the angle the forearm adds, both from atan2.
Rounding can push the cosine just past one on the boundary, so it is clamped within a tolerance, and a zero sine returns one solution rather than two identical ones. The Jacobian determinant is l1 l2 sin(q2), zero when straight or folded. With equal links and the target at the base, any shoulder angle works; mention it. The harness checks round trips from random joint angles and the Jacobian against finite differences.
- Constant-velocity Kalman filter with outlier gatingMedium · Python
Write a Kalman filter for an object moving in one dimension with state position and velocity, a white-noise acceleration process model and position measurements. Implement predict with a variable time step and update with a measurement variance. Return the normalised innovation squared from update, and if a gate threshold is given, reject the measurement when it is exceeded. Keep the covariance symmetric. Use only the standard library; write the 2 by 2 algebra out.
Hints
- For the white-noise acceleration model with spectral density q, Q is q times [[dt cubed over 3, dt squared over 2], [dt squared over 2, dt]].
- With H equal to [1, 0], the innovation variance is P[0][0] plus R and the gain is the first column of P divided by it.
Solution and explanation
class CvKalman1D: """Constant-velocity Kalman filter. State [p, v]; measures position. q is the white-acceleration spectral density, r the measurement variance.""" def __init__(self, p0, v0, P0, q, r): self.x = [p0, v0] self.P = [row[:] for row in P0] self.q, self.r = q, r def predict(self, dt): p, v = self.x self.x = [p + v * dt, v] (a, b), (c, d) = self.P # P = F P F^T with F = [[1, dt], [0, 1]] a2 = a + dt * (b + c) + dt * dt * d b2 = b + dt * d c2 = c + dt * d q = self.q # Q for a white-noise acceleration model self.P = [[a2 + q * dt ** 3 / 3.0, b2 + q * dt ** 2 / 2.0], [c2 + q * dt ** 2 / 2.0, d + q * dt]] def update(self, z, gate=None): """Returns the normalised innovation squared. If gate is given and the NIS exceeds it, the measurement is rejected as an outlier.""" (a, b), (c, d) = self.P y = z - self.x[0] # innovation s = a + self.r # innovation variance, H = [1, 0] nis = y * y / s if gate is not None and nis > gate: return nis k0, k1 = a / s, c / s # Kalman gain P H^T / S self.x = [self.x[0] + k0 * y, self.x[1] + k1 * y] # Joseph form keeps P symmetric and positive semi-definite. # (I - K H) P (I - K H)^T + K R K^T, written out for 2x2. i00, i10 = 1.0 - k0, -k1 n00 = i00 * i00 * a + self.r * k0 * k0 n01 = i00 * (i10 * a + b) + self.r * k0 * k1 n11 = i10 * i10 * a + i10 * (b + c) + d + self.r * k1 * k1 self.P = [[n00, n01], [n01, n11]] return nisPredict applies F and computes F P F-transpose plus Q, where Q comes from integrating white acceleration noise over the step, so irregular time steps are handled correctly. Update forms the innovation, its variance S and the gain, then corrects the state. The covariance uses the Joseph form, which stays symmetric and positive semi-definite despite rounding. The harness compares every step with a generic matrix implementation.
The normalised innovation squared, y squared over S, follows a chi-squared distribution with one degree of freedom for a consistent filter, so its long-run mean should be about one; the harness checks that. If it is well above one, Q or R is too small. The gate rejects unlikely measurements, for example above 6.63 for 99 per cent. Point out that an overconfident filter can reject everything, so rejections must be counted.
- Wait-free single-producer single-consumer queue in C++Hard · C++
A 1 kHz control thread must hand samples to a logging thread without locks, allocation or blocking. Write a bounded single-producer single-consumer queue template for trivially copyable types. try_push returns false when full, try_pop returns an empty optional when empty. Explain each memory ordering you use, and how you avoid false sharing.
Hints
- Let the indices grow without bound and mask them with N minus 1; then full is head minus tail equal to N.
- Each side can cache the other side's index and only reload it with acquire ordering when the queue looks full or empty.
Solution and explanation
#include <array> #include <atomic> #include <cstddef> #include <optional> #include <type_traits> // Wait-free single-producer single-consumer queue, for example from a // real-time control thread to a logging thread. N must be a power of two. template <typename T, std::size_t N> class SpscQueue { static_assert(N >= 2 && (N & (N - 1)) == 0, "N must be a power of two"); static_assert(std::is_trivially_copyable_v<T>, "keep the RT path allocation-free"); public: // Producer thread only. Returns false when full (caller counts the drop). bool try_push(const T& v) { const std::size_t head = head_.load(std::memory_order_relaxed); if (head - tail_cache_ == N) { // looks full tail_cache_ = tail_.load(std::memory_order_acquire); if (head - tail_cache_ == N) return false; } buf_[head & (N - 1)] = v; head_.store(head + 1, std::memory_order_release); // publish the slot return true; } // Consumer thread only. std::optional<T> try_pop() { const std::size_t tail = tail_.load(std::memory_order_relaxed); if (tail == head_cache_) { // looks empty head_cache_ = head_.load(std::memory_order_acquire); if (tail == head_cache_) return std::nullopt; } T v = buf_[tail & (N - 1)]; tail_.store(tail + 1, std::memory_order_release); // free the slot return v; } private: static constexpr std::size_t kLine = 64; // Indices grow without bound and are masked on use; size_t wrap-around // is harmless because only differences are compared. alignas(kLine) std::atomic<std::size_t> head_{0}; // written by producer alignas(kLine) std::size_t tail_cache_ = 0; // producer's copy alignas(kLine) std::atomic<std::size_t> tail_{0}; // written by consumer alignas(kLine) std::size_t head_cache_ = 0; // consumer's copy alignas(kLine) std::array<T, N> buf_{}; };Only the producer writes head and only the consumer writes tail, so no read-modify-write is needed. The producer writes the slot, then release-stores head; the consumer acquire-loads head before reading the slot, so the contents are visible. Symmetrically, the consumer releases tail after copying out, and the producer acquires tail before reusing a slot. Each side reads its own index relaxed.
Each side caches the other's index and reloads it only when the queue looks full or empty, which avoids touching the other core's cache line on most calls. Indices, caches and buffer sit on separate cache lines to prevent false sharing. Free-running indices make all N slots usable, and unsigned wrap-around is harmless because only differences are compared. Both operations are wait-free and constant time. The harness pushes five million sequenced items under the thread sanitiser. On full, the producer counts a drop and never blocks.
- Point-to-point ICP for 2D scan matchingHard · Python
Write 2D iterative closest point. Given a source scan and a target scan as lists of (x, y) points, find the rotation and translation that align the source to the target. Reject correspondences further than a maximum distance. Return the accumulated transform and the final RMS error. Use the closed-form least-squares solution for each step, without a linear algebra library.
Test: generate a target scan, transform it by a known small rotation and translation, add noise and an outlier, and recover the transform.
Hints
- In 2D, after subtracting centroids, the best rotation angle is atan2 of the sum of cross products over the sum of dot products.
- Compose each increment onto the running total: the new translation is the increment's rotation applied to the old translation, plus the increment's translation.
- Stop when the increment is tiny, not when the error stops changing.
Solution and explanation
import math def best_rigid_2d(src, dst): """Closed-form least-squares (theta, tx, ty) mapping src[i] onto dst[i].""" n = len(src) sx = sum(p[0] for p in src) / n sy = sum(p[1] for p in src) / n dx = sum(q[0] for q in dst) / n dy = sum(q[1] for q in dst) / n num = den = 0.0 for (px, py), (qx, qy) in zip(src, dst): ax, ay, bx, by = px - sx, py - sy, qx - dx, qy - dy num += ax * by - ay * bx # sum of cross products den += ax * bx + ay * by # sum of dot products th = math.atan2(num, den) c, s = math.cos(th), math.sin(th) return th, dx - (c * sx - s * sy), dy - (s * sx + c * sy) def transform(pts, th, tx, ty): c, s = math.cos(th), math.sin(th) return [(c * x - s * y + tx, s * x + c * y + ty) for x, y in pts] def icp(src, dst, max_dist=1.0, iters=100, tol=1e-7): """Align scan src to scan dst. Returns the accumulated (theta, tx, ty) and the final RMS error over accepted pairs.""" th = tx = ty = 0.0 cur = list(src) rms = math.inf for _ in range(iters): pairs_s, pairs_d, sq = [], [], 0.0 for p in cur: # brute-force nearest neighbour q = min(dst, key=lambda d: (d[0] - p[0]) ** 2 + (d[1] - p[1]) ** 2) d2 = (q[0] - p[0]) ** 2 + (q[1] - p[1]) ** 2 if d2 <= max_dist * max_dist: # reject far pairs as outliers pairs_s.append(p) pairs_d.append(q) sq += d2 if len(pairs_s) < 3: raise RuntimeError("too few correspondences; bad initial guess") rms = math.sqrt(sq / len(pairs_s)) dth, dtx, dty = best_rigid_2d(pairs_s, pairs_d) cur = transform(cur, dth, dtx, dty) c, s = math.cos(dth), math.sin(dth) # compose increment onto total th, tx, ty = th + dth, c * tx - s * ty + dtx, s * tx + c * ty + dty if abs(dth) < tol and math.hypot(dtx, dty) < tol: break # the increment has vanished return (th, tx, ty), rmsEach iteration pairs every source point with its nearest target point, drops pairs beyond the threshold, and solves for the rigid transform minimising squared distances. In 2D, after centring both sets, the angle is atan2 of the summed cross products over the summed dot products, and the translation maps one centroid onto the other: the 2D case of the SVD solution. The increment is applied and composed onto the total.
Brute-force matching costs O(nm) per iteration; a kd-tree brings it to about O(n log m). ICP is local and needs a reasonable initial guess. Convergence is judged on the increment, because the RMS error can plateau while the estimate still moves. When both scans are sampled on the same regular grid along walls, point-to-point matching can stall on a shifted alignment; point-to-line matching avoids that and converges in fewer iterations, the follow-up interviewers expect.
Design and debugging scenarios
Talk through each one out loud for 15–20 minutes, then compare your answer with the model walk-through.
- Design the software for a vision-guided bin-picking cellSystem design
A customer wants a six-joint arm with a suction gripper to pick mixed parcels from a tote and place them on a conveyor. A depth camera is mounted above the tote. Design the software: components, data flow, how you choose a grasp, how you plan motion, and how the system handles failure. Target throughput is several hundred picks per hour.
Model walk-through
Requirements first. I would ask about parcel sizes, weights and materials, since suction fails on porous or deformable surfaces; the pick rate and reliability needed; whether people work nearby; and what happens to a failed pick. Several hundred picks per hour leaves a cycle of a few seconds, so perception and planning must each take well under a second and overlap with arm motion.
Components. A camera driver publishes depth and colour images with hardware timestamps. Perception segments the tote into candidate items, with a learned model plus geometric checks on the point cloud, and estimates surface normals. A grasp planner scores suction points by patch flatness and size, distance from the item's centre of mass, gripper clearance and reachability. Motion planning produces a collision-free path into the tote, a straight approach along the normal and a transfer to the conveyor, using a model of the tote walls and a distance field from the depth image. A cell controller, a state machine or behaviour tree, sequences the cycle and reads the vacuum sensor.
Calibration and throughput. Every grasp depends on the hand-eye calibration, so it is checked each shift against a reference marker. While the arm transfers one parcel, the camera captures and processes the next scene, hiding perception behind motion, and trajectories are time-optimal within joint limits.
Failure handling. The vacuum sensor confirms a seal before lifting and detects a drop in transfer. A failed pick retries with the next-best grasp, then asks for help after a limit. Each failure is logged with its image and chosen grasp, which becomes test and training data.
Safety and testing. Guarding or safety-rated scanners stop the arm independently of the cell software. I would test perception and grasp scoring offline on recorded scenes, sequencing in a simulated cell, and measure picks per hour and failures per thousand picks on the real cell with representative parcels.
- Perception latency grows the longer the robot runsDebugging
A mobile robot's obstacle detection node processes a 3D lidar at 10 Hz. After the robot boots, the delay between a scan's timestamp and the obstacle message is about 40 ms. After an hour it is over 600 ms, and the robot has started to brake late. Restarting the node fixes it for a while. How do you find and fix the cause?
Model walk-through
Frame the symptom. Latency that grows with uptime and resets on restart points to either a queue the node no longer keeps up with, or state accumulating inside the process. I separate the two by logging three times per scan: the scan stamp, the callback start and the publish time. A growing wait with constant processing time means queuing; growing processing time means something inside is getting slower. I also log memory and CPU.
Queuing. If processing sometimes exceeds the 100 ms period, a deep subscriber queue fills and the node works on scans that are already old. The fix is to make processing faster and set the history depth to one or two, so the node always handles the newest scan. Old obstacle data is worse than none.
Accumulating state. If processing time grows, I look for containers that never shrink: a tracker that never deletes tracks, an identifier map that is never pruned, a debug buffer, or a search structure rebuilt over every point ever seen. Rising memory confirms it, and a profiler attached after an hour shows the hot function.
Environment. Thermal throttling or another process growing over time can also slow the node, so I check CPU frequency and temperature in the same logs.
Fix and prevent. I add a monitor that publishes stamp-to-output delay and warns above a threshold, and a soak test that replays an hour of data and fails if latency or memory trends upward. The local planner should also treat obstacle data older than a set age as missing and slow the robot, which would have made this fault safe before it was found.
- Review a laser scan callbackCode review
A colleague asks you to review this ROS 2 C++ callback from an obstacle tracker. The node uses the default single-threaded executor. What would you comment on, in order of importance?
void Tracker::onScan(const sensor_msgs::msg::LaserScan::SharedPtr scan) { auto tf = buffer_.lookupTransform("map", scan->header.frame_id, tf2::TimePointZero); std::vector<Point> pts; for (size_t i = 0; i < scan->ranges.size(); ++i) { float a = scan->angle_min + i * scan->angle_increment; pts.push_back(transform(tf, scan->ranges[i] * cos(a), scan->ranges[i] * sin(a))); } auto req = std::make_shared<GetPlan::Request>(); auto fut = client_->async_send_request(req); fut.wait(); heading_ += cmd_.angular.z * 0.1; publishMarkers(pts); }Model walk-through
1. Deadlock. fut.wait() blocks inside a callback. With a single-threaded executor the response can only be processed by the thread that is waiting, so the node hangs on the first scan. Handle the response asynchronously, or put the client in its own callback group on a multi-threaded executor. I would also ask why a scan callback calls a planner at 10 Hz.
2. Wrong transform time. Looking up the latest transform pairs the scan with the newest pose, not the pose when it was taken, so obstacles smear on a turning robot. Look up at the scan stamp with a short timeout, or use a message filter.
3. Unhandled exceptions. lookupTransform throws when the transform is missing, which is normal at start-up, and uncaught it kills the node. Catch it, warn with throttling and skip the scan.
4. Invalid ranges. Infinite, NaN and out-of-limit ranges are transformed as if valid, producing obstacles at infinity or at the sensor. Filter against range_min and range_max first.
5. Heading integration. heading_ uses a fixed 0.1 s step, which is only right at exactly 10 Hz, and is never wrapped. Use the real interval and wrap the angle, or better, take heading from odometry. cmd_ may be written by another callback, which becomes a data race if the threading changes.
6. Frame choice. Tracking in map makes every localisation correction move all tracks; a tracker usually works in odom.
7. Smaller points. Reserve the vector, since its size is known; compute the angle in double; note that motion during the scan is ignored, which matters at speed.
I would ask for a unit test with a synthetic scan that includes invalid ranges, and point the author to AI code review round for more review practice.
- Choose a localisation approach for a robot moving between a warehouse and an outdoor yardResearch
Your team is starting a new tugger robot that pulls carts inside a large warehouse, where racking layouts change weekly, and across an outdoor yard between buildings. The current product uses 2D lidar localisation against a fixed map indoors only. You have two weeks to recommend a localisation approach. How do you run the investigation and what do you expect to recommend?
Model walk-through
Define success first. I would write down the accuracy needed for docking with carts and passing through doors, the acceptable failure rate, recovery time, the computing budget and constraints such as no new infrastructure in the yard. Needs differ by zone, so I state them per zone.
Understand the environments. Indoors, racking moves weekly, so a fixed 2D map degrades, but walls, columns and roof structure stay. Outdoors, a 2D lidar sees little in an open yard, while satellite positioning works except near buildings, where multipath appears. Weather and light affect cameras.
Collect data early. In the first days I would drive a prototype carrying a 3D lidar, the existing 2D lidar, an IMU, wheel odometry and an RTK-capable receiver through both areas at different times, with good time synchronisation, and survey reference markers for ground truth.
Compare candidates offline. First, 3D lidar localisation against a map built from features above racking height, which stay put when racking moves. Second, the same with satellite positioning fused outdoors and in transitions. Third, fiducial markers at docking points for final accuracy. I would measure error against the reference, failure and recovery behaviour, and CPU use.
Likely recommendation. A fusion design: wheel odometry and IMU for a smooth odom frame; 3D lidar localisation with change detection indoors; satellite positioning fused in the yard with innovation gating near buildings; and markers or local scan matching at docking points. I would present the comparison with its limits, such as only two weeks of weather, and flag the risks: map upkeep as layouts change, indoor-outdoor transitions and sensor cost.
Run a mock interview
Ask a friend to play the interviewer, or record yourself. Pick questions and an exercise you have not opened yet.
| Minutes | Stage | What to do |
|---|---|---|
| 0–5 | Introduction | Give a two-minute summary of one robot project: the robot, your part of the software, one hard problem you solved and how you measured the result. Practise until it is concrete and short. |
| 5–15 | Fundamentals rapid fire | Pick six questions at random from different sections and answer each in about ninety seconds, out loud. Include at least one on frames or rotations and one on ROS 2 timing. Check against the model answers afterwards. |
| 15–35 | Coding | Solve one Medium exercise from this page without looking at the solution: state the conventions and edge cases first, write the code, then write three test cases including an awkward one such as angle wrap, an unreachable goal or a zero-weight particle. Narrate your reasoning. |
| 35–45 | Maths on paper | Derive one result by hand: the forward kinematics and Jacobian of a two-link arm, the stereo depth error, or the Kalman update for a scalar state. Check units and a limiting case. |
| 45–55 | Design or debugging scenario | Take one scenario from this page. Spend two minutes on requirements and questions, draw the block diagram or the investigation plan, then cover failure modes, safety and how you would test it. |
| 55–60 | Review | Write down the two weakest answers and what a strong answer would have contained. Revisit those sections before the next mock. Ask a friend to play the interviewer on the next run and to push with follow-up questions. |
Cheat sheet
Facts worth having at your fingertips on the day.
- Transform naming
- T_A_B is frame B expressed in A and maps p_B to p_A; inner letters cancel when composing: T_A_C = T_A_B T_B_C.
- Mobile frames
- map to odom to base_link: odom is smooth and drifts, map is globally correct and may jump; localisation publishes map to odom.
- Quaternion basics
- From axis u and angle a: (cos(a/2), u sin(a/2)); q and minus q are the same rotation; renormalise after many products.
- Body-rate integration
- q_next = q times exp(omega dt / 2) for a gyro rate in the body frame, after subtracting the bias.
- Differential drive
- v = (v_r + v_l) / 2 and omega = (v_r minus v_l) / track width.
- Bicycle model
- theta-dot = v tan(delta) / wheelbase; minimum turning radius is wheelbase / tan(max steering angle).
- Jacobian
- Maps joint velocities to tool twist; its transpose maps tool wrench to joint torques; rank loss means a singularity.
- Damped least squares
- dq = J-transpose (J J-transpose + lambda squared I) inverse times the error; bounded steps near singularities.
- Arm dynamics
- M(q) q-double-dot + C(q, q-dot) q-dot + g(q) = tau; recursive Newton–Euler is linear in the joint count.
- Pinhole
- u = fx X/Z + cx and v = fy Y/Z + cy; distortion is radial plus tangential.
- Stereo
- Z = f B / d; depth error grows with Z squared over f B.
- Kalman predict
- x = F x, P = F P F-transpose + Q.
- Kalman update
- S = H P H-transpose + R, K = P H-transpose S-inverse, x += K y; prefer the Joseph form for P.
- White-noise acceleration Q
- For state [p, v]: q times [[dt cubed/3, dt squared/2], [dt squared/2, dt]].
- Chi-squared gates
- 95 and 99 per cent: 3.84 and 6.63 for one dimension, 5.99 and 9.21 for two, 7.81 and 11.34 for three.
- Effective sample size
- 1 over the sum of squared normalised weights; resample when it falls below about half the particle count.
- Log-odds occupancy
- Add a constant per hit or pass-through; p = 1 minus 1/(1 + e to the l); clamp l so the map can change.
- A* optimality
- Admissible heuristic gives an optimal path; consistent also means each node is expanded once.
- Sampling planners
- RRT is probabilistically complete; RRT* is also asymptotically optimal; RRT-Connect grows two trees.
- Control rate
- Sample about ten to twenty times the closed-loop bandwidth; tune cascaded loops from the inside out.
- Stability margins
- Aim for a phase margin of about 45 degrees or more and a gain margin of about 6 dB or more.
- QoS matching
- A reliable reader does not match a best-effort writer; a transient-local reader does not match a volatile writer.
- tf2 buffer
- Default cache is ten seconds; lookups interpolate between stamps and fail rather than extrapolate.
- Real-time thread
- SCHED_FIFO priority, mlockall, isolated core, absolute-deadline clock_nanosleep, no allocation or blocking I/O.
- SPSC ordering
- Write the slot then release-store the index; acquire-load the other index before reading the slot.
Red flags that sink candidates
Being vague about frames, such as saying you transform the point into the world without naming which transform, which direction and at what time. Always name both frames and the timestamp.
Treating ROS 2 as a black box: knowing the commands but not discovery, QoS matching or executors. When asked why something does not arrive, reason from how the middleware works.
Quoting formulas without the assumptions, such as calling the Kalman filter optimal without saying for what model. Follow each formula with when it fails.
Proposing to fix timing problems by sleeping, or extrapolation errors by using the latest transform. Interviewers read this as hiding bugs; wait for the data or fix the clock instead.
Putting safety in the planner or in Python code on the main computer. State that safety functions are independent, lower in the stack and often in hardware.
Answering every debugging question with a guess. Lead with how you would make the problem observable, then form and test hypotheses from the evidence.
Writing coding-round solutions with no edge cases: no angle wrap, no unreachable goal, no division-by-zero guard. Say the edge cases before coding and test them after.
Overstating results: claiming a system worked well without any metric. Bring numbers you measured, such as loop jitter, localisation error or picks per hour, and how you measured them.
FAQ
Do I need ROS experience to get a robotics software engineer job?
Most job descriptions mention ROS or ROS 2, and interviewers will ask about it, but strong fundamentals matter more. If you have not used it, build one small project with it before applying, such as a simulated mobile robot with a launch file, a custom node, tf frames and a recorded bag, so you can talk about real problems you met. Some companies use their own middleware; for them, show you understand publish-subscribe, timing and serialisation in general.
How much maths is asked in robotics software engineer interviews?
Expect linear algebra, rotations, basic probability and simple calculus, applied rather than proved. Typical requests are to compose transforms, derive two-link kinematics, explain the Kalman filter steps, or estimate how stereo depth error grows with range. Specialist teams, such as controls or perception, go deeper in their area. Practise deriving on paper and explaining the meaning of each term.
Should I use C++ or Python in the robotics coding round?
Use the language the role uses most if you are fluent in it, which is usually C++. If the interviewer allows either and the problem is algorithmic, such as A* or ICP, Python lets you focus on the algorithm. For questions about real-time code, memory or concurrency, use C++, because that is what is being tested. In either case, write clean, tested code and state conventions such as frames and angle ranges. Brush up with Modern C++ Roadmap and Data Structures and Algorithms Roadmap.
What projects help most when preparing for robotics interviews?
Projects where you implemented the core algorithm yourself and measured it: a particle filter localising a simulated robot, an EKF fusing odometry and an IMU with plotted innovations, a planner with a tested collision checker, or a real small robot with a tuned control loop. Interviewers ask what went wrong and how you knew, so keep logs, plots and numbers. A wrapper around a library with no measurements does not give you that material.
How is a robotics software interview different from a general software engineering interview?
The coding round is similar but often has a geometric or numerical twist, and the domain rounds test maths and physical intuition as well as software. Design questions are about whole robots, so timing, sensors, failure modes and safety carry as much weight as scalability. Debugging questions describe physical symptoms. General preparation for Software Engineer still helps, and the embedded side overlaps with Embedded Systems Engineer.