angularsen

Top repositories

angularsen’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside angularsen’s account where angularsen opened or reviewed pull requests in the past 365 days.

Who noticed angularsen

Everyone who starred or forked angularsen’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

1post
Nov 41 post
  1. Mars Climate Orbiter: The Unit Bug That Lived Between Two Teams (and How to Stop It in C#) --- title: "Mars Climate Orbiter: The Unit Bug That Lived Between Two Teams (and How to Stop It in C#)" published: true description: What the NASA investigation board actually found about the lost Mars Climate Orbiter, why "they forgot to convert to metric" is the wrong lesson, and how to keep units attached to numbers in .NET. tags: dotnet, csharp, softwareengineering, programming cover_image: canonical_url: series: Vlad's Stack --- In September 1999, a NASA spacecraft finished a trip of 669 million kilometers to Mars. It had one job left: fire its engine at the right height and slip into orbit. It came in about 170 kilometers lower than planned, low enough to hit the Martian atmosphere, and nobody heard from it again. The engine was fine. The sensors were fine. The software on the spacecraft was fine. The bug was in a file on Earth, passed between two teams, full of numbers in the wrong unit. {% youtube youtube.com %} You've probably heard the short version: "NASA forgot to convert to metric." That's not quite what the Mishap Investigation Board's Phase I report (November 10, 1999) says, and the real version is a lot more useful if you write software for a living. Two teams, one file The work was split between two places. The spacecraft team at Lockheed Martin in Denver flew the spacecraft. The navigation team at NASA's Jet Propulsion Laboratory in California worked out where it was. To keep pointing the right way, the orbiter used reaction wheels, which slowly pick up spin, partly from sunlight pushing on the spacecraft. When the wheels spin too fast, small thrusters fire to dump the extra momentum. NASA calls these angular momentum desaturation (AMD) events. Each one also nudges the spacecraft's path a little, so the navigators need to know every one of them: how long it fired, and how hard. After each event, a ground program called SM_FORCES ("small forces") computed the impulse and wrote it to the AMD file. The navigators read that file to model the trajectory. There was a Software Interface Specification for that file. It said the impulse was in newton-seconds. SM_FORCES wrote pound-force seconds. The file itself had no unit in it, just numbers. Since 1 lbf ≈ 4.45 N, every push was read as about 4.45 times weaker than it really was. The cruel detail: the spacecraft's own software did the same calculation, in metric, and got it right. The report is explicit that the mismodeling happened only in the ground software. Why it got so big One small push read wrong doesn't matter. This one mattered because of the spacecraft's shape. The orbiter had one solar panel, on one side only. Sunlight pushed on it like wind on a sail, so the wheels filled up with momentum much faster than on its predecessor, Mars Global Surveyor. A daily 180° flip ("barbecue mode") had been planned to cancel that out, but later trade studies decided it wasn't needed and it was dropped. Nobody told the navigators. In the end, the desaturation events happened 10 to 14 times more often than the navigation team expected. Thousands of small underestimates, over nine months, all pushing the same way. The warnings This is the part I find most familiar. - For the first four months, the AMD files had format errors and couldn't be used at all. The contractor emailed the navigators each time an event happened, and they modeled the effect on their own. - In April 1999 the files were fixed. Within about a week, the data suggested the pushes were being underestimated. - It was hard to confirm from Earth: most of each push was perpendicular to the line of sight, so the Doppler tracking barely saw it. - Through spring and summer, the board says, "concerns existed at the working level". The team worked it out over email, not through NASA's formal Incident, Surprise, Anomaly (ISA) process. - The spacecraft itself was computing and sending its own velocity-change data for these events the whole time. The navigators didn't know that until long after launch. The board notes that an early comparison might have exposed the units problem. The last week On September 15, the last planned correction targeted a closest pass of 226 km. Over the next week, the estimate slipped to 150–170 km. Solutions using only Doppler data kept putting the path closer to the planet than the others did, and that disagreement was never resolved. About an hour before arrival, new tracking data said the pass could be as low as 110 km. The minimum the orbiter could survive was 80 km. A contingency maneuver (TCM-5) existed on paper. It was discussed verbally shortly before arrival, but it had never been prepared or tested, and the on-board arrival sequence was already running. It wasn't executed. On September 23, a little after 2 a.m. California time, the main engine fired. About four minutes in, the orbiter went behind Mars, 49 seconds earlier than predicted. It was due back about 21 minutes later. It never came back. On September 29 the team found the factor of 4.45. Reconstructed with the right numbers, the closest pass had been about 57 km. A week after the loss, NASA's associate administrator for space science said the problem wasn't the error itself but that NASA's systems engineering and checks didn't catch it (JPL release 99-113). The board agreed: mistakes happen on every project, and the process is there to catch them. The same bug in C# In most C# code, an impulse (or a price, a duration, a distance) is just a double. The compiler can't tell pounds from newtons. 4.45 is just a number. Give each unit its own type public readonly record struct NewtonSeconds(double Value) { public static NewtonSeconds operator +(NewtonSeconds a, NewtonSeconds b) => new(a.Value + b.Value); } public readonly record struct PoundForceSeconds(double Value) { // 1 lbf = 0.45359237 kg * 9.80665 m/s^2 = 4.4482216152605 N (exact, by definition) private const double NewtonSecondsPer = 4.4482216152605; public NewtonSeconds ToNewtonSeconds() => new(Value * NewtonSecondsPer); } public static class Navigation { public static void RecordDesaturation(NewtonSeconds impulse) { /* ... */ } } Now the Mars mistake doesn't compile: var fromGround = new PoundForceSeconds(0.25); Navigation.RecordDesaturation(fromGround); // CS1503: cannot convert PoundForceSeconds to NewtonSeconds Navigation.RecordDesaturation(fromGround.ToNewtonSeconds()); // the one way across, written once, tested once Or don't write it yourself UnitsNet (MIT) already has an Impulse quantity with both units, newton-second and pound-force-second (unit definitions): using UnitsNet; Impulse fromFile = Impulse.FromPoundForceSeconds(0.25); double ns = fromFile.NewtonSeconds; // ~1.112 F# has it in the language F# runs on .NET too, and units of measure are part of the language: [<Measure>] type N [<Measure>] type lbf [<Measure>] type s let a = 10.0<N s> let b = 2.0<lbf s> let total = a + b // compile error: the units don't match The catch: types stop at the boundary Here's the part that would not have saved the orbiter. All of these types live inside one program. The F# docs say it directly: units of measure are checked at compile time and are not kept at run time. Write the number to a file, or send it to another service, and the type is gone. It's just digits again. That's exactly where Mars Climate Orbiter broke: not in the math, but in the file between two teams. So at every boundary: 1. The unit travels with the number. Put it in the field name, where nobody can miss it: public sealed record DesaturationEvent( DateTimeOffset At, [property: JsonPropertyName("impulse_newton_seconds")] double ImpulseNewtonSeconds); 2. Test the boundary itself, against a number you already trust. Not "does it parse", but "does it agree with an independent source": [Fact] public void Amd_file_agrees_with_spacecraft_telemetry() { var fromFile = AmdFile.Parse(File.ReadAllText("fixtures/amd-sample.txt")); var fromSpacecraft = Telemetry.Load("fixtures/telemetry-sample.json"); Assert.Equal(fromSpacecraft.DeltaV, fromFile.DeltaV, tolerance: 0.01); } The orbiter was sending that second number home the whole time. Three things to take from this 1. A number without a unit is a guess. Make the unit part of the type inside your code, and part of the name at the edges. 2. A worry in an email isn't a bug report. Write it down where it can't get lost. 3. When two answers disagree, that's not noise. The Doppler-only solutions were telling the team something for weeks. Mars Climate Orbiter wasn't lost to hard math. It was lost to a plain number crossing from one team to another with no unit on it. Where does a number cross from one team to another in your system, with no unit attached? Tell me in the comments. --- Sources: NASA Mars Climate Orbiter Mishap Investigation Board, Phase I Report, Nov 10, 1999; NASA/JPL release 99-113, Sep 30, 1999; NASA/JPL Mars Climate Orbiter Arrival press kit, Sep 1999; Microsoft Learn, F# Units of Measure; UnitsNet. I make Vlad's Stack, short stories and explainers about how the tools you use every day actually work, for people who write code: youtube.com

Achievements

Awards earned by the repositories angularsen owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.