Hacker News new | past | comments | ask | show | jobs | submit login
Show HN: SpaceX Dragon simulator docking autopilot in Clojure (github.com/danirukun)
181 points by danpetrov on May 21, 2020 | hide | past | favorite | 35 comments



I have a question about control flow of the overall system. Note that I haven't actually run this or debugged it. Or frankly, thought about it for more than a minute or two. Its more of an architectural question.

So in dragon.clj there's a case statement to execute commands in the translate function such that the command to go down or left in an abstract calculated sense is implemented as hitting a virtual down or left keyboard key.

What happens if you have to go down AND left?

I was just thinking why not pass a Clojure list to the translate function and when list contains a down it hits the virtual s key AND if the list contains a left command it hits a virtual a key, then you could wiggle "down-left" at the same time.

I mean, obviously it works and sometimes architectural decisions have no technical reason and are arbitrary, which would be perfectly OK. I was just idly curious.

Clojure is cool, like programmer catnip, so I had to look at it.


Heh, never thought about it that way! But good question. Yes, it can translate on 2 axes at the same time, since the control loops for each axis run in separate threads, so there is no control locking. The same is with rotation - it can e.g. pitch, roll and yaw at the same time.


So if you click one of the rotate buttons after it has zeroed out the offsets will it correct? Is it using a PID model or something else?


The SpaceX simulation is simplistic enough and gives you enough data that you really don't need a PID controller to hit the target numbers.


Totally agree I’m just trying to learn PID control and seeing a few different implementations in javascript (to solve the same problem) has actually been quite informative.


Just from a layman, this is so freaking cool, thank you for writing it and sharing it!


https://www.reddit.com/r/spacex/comments/gigmfh/spacex_iss_d... does it in one line but it's kind of cheating.


Definitely cheating, it's not autopilot. It is just updating your co-ordinates every 500ms on a straight line.

I would like to see a real autopilot written in javascript that you can paste into the console.


I wrote an autopilot[0] for the simulator in JS, which sends key events to the game.

[0]: https://gist.github.com/ForestKatsch/01069f29e8316df11774025...


This is awesome! As a recent student of PIDs, it's even better after uncommenting the createPidDisplay statement on line 566 :)

Only feature request is a 'Do a Barrel Roll!' button.


It's cheating but not as much. It's setting the "motion vector" aka setting the speed at current instant. It's just skipping the controls which are normally setting the "motion vector". For the orientation it's totally cheating though using camera.lookat(target).


Anything is a oneliner when you erase all the `\n`s


Python begs to differ


You can add semicolons at the end of python lines to replace \n's when calling from the command line.

  python3 -c "n=1;print(n)"


How does it deal with code blocks normally defined by indentation? (As an aside, indentation being part of syntax is the only thing keeping me disgusted with python - I like the language otherwise.)


it doesn't :) though it works fine if your terminal allows multiline input:

  python -c "
  for x in stuff:
    this(x)
    that(x)
  "
at some point i even wrote a hacky little wrapper that let you use braces and replaced¹ them with spaces+newlines, something like this:

  pyc-wrap "for x in stuff { this(x); that(x); }"
i can dig the code up if you need it :)

========

though after that i sort of went of the deep end adding perl-style "it" ($_) variables for loop variables:

  for stuff { this($); that($);
(yes, closing braces at the end were optional...)

and for the result of the previous line:

  2; $+1; hex($);
  # roughly translates to:
  x = 2; x = x+1; x = hex(x);
surprisingly pleasant to use for shell-like tasks actually!

edit: almost forgot - if during execution you used a name `foo` not found in locals/globals, `foo` was treated as an external command, and resolved to something like

  lambda *args: os.system('foo', *args)
so that you could (sorta) easily call external commands. fun times :)

---

¹ slightly more involved than a dumb code.replace('{', ':\n') because you need to maintain the indent level... but around that level of sophistication


browser console is a pretty under rated feature. I wish it was used more extensively to teach people how to code. so many cool little hacks you can do on public web pages that cement new concepts quickly.


Very simple P-controller based autopilot derived from the one-liner:

  let a = setInterval(() => { 
      // retrieve values from simulation
      let roll = fixedRotationZ;
      let pitch = fixedRotationX;
      let yaw = fixedRotationY;
      let rpy = new THREE.Vector3(roll, pitch, yaw);
  
      let rollRate = -rateRotationZ/10.0;
      let pitchRate = -rateRotationX/10.0;
      let yawRate = -rateRotationY/10.0;
  
      let rpyRate = new THREE.Vector3(rollRate, pitchRate, yawRate);
  
      let pos = camera.position.clone();
      let posRate = motionVector.clone();
      let targetPos = issObject.position.clone();
  
      let targetRpy = new THREE.Vector3(0,0,0);
      let dRpy = targetRpy.clone().sub(rpy);
  
      // P-controller controls target roll,pitch,yaw-rate (targetRpyRate) to reach target orientation
      let targetRpyRate = new THREE.Vector3(
          dRpy.x/1.0, 
          dRpy.y/1.0, 
          dRpy.z/1.0
      );
  
      let minRpyRate = new THREE.Vector3(-1.5, -1.5, -1.5);
      let maxRpyRate = new THREE.Vector3(+1.5, +1.5, +1.5);
      targetRpyRate.clamp(minRpyRate, maxRpyRate)
  
      let dRpyRate = targetRpyRate.clone().sub(rpyRate);
   
      // don't calculate with displayed x,y,z but the internal coordinates 
      // (z-axis is forward and backward)
      let dPos = targetPos.clone().sub(pos);
      let d = dPos.length();
      // P-controller controls target motion (targetPosRate) to reach target position
      let targetPosRate = new THREE.Vector3(
          dPos.x / 100.0,
          dPos.y / 100.0,
          dPos.z / 100.0
      );
      let minPosRate = new THREE.Vector3(-0.1, -0.1, -0.01);
      let maxPosRate = new THREE.Vector3(+0.1, +0.1, +0.01);
  
      // thresholds for bang contRol
      let dRpyRateControlThreshold = new THREE.Vector3(0.0, 0.0, 0.0);
      let dPosRateControlThreshold = new THREE.Vector3(0.001, 0.001, 0.001);
  
      // define some phases where we want to have different movement behaviour
  
      if (dRpyRate.length() > 0.1)
      {
          // not correctly oriented, stop movement
          targetPosRate.x = 0;
          targetPosRate.y = 0;
          targetPosRate.z = 0;
      }
      else if (Math.abs(dPos.z) < 20)
      {
          // we are very close, slow approach, larger P-control in lateral
          // directions to correct remaining errors
          minPosRate.z = -0.005;
          maxPosRate.z = +0.005;
          dPosRateControlThreshold.x = 0.01;
          dPosRateControlThreshold.y = 0.01;
          targetPosRate.x = dPos.x;
          targetPosRate.y = dPos.y;
      }
      else if (Math.abs(dPos.z) < 50)
      {
          // we are getting, close, slow down
          minPosRate.z = -0.01;
          maxPosRate.z = +0.01;
  
          minPosRate.x = -0.01;
          minPosRate.y = -0.01;
          maxPosRate.x = +0.01;
          maxPosRate.y = +0.01;
      }
      else if (Math.abs(dPos.z) < 100)
      {
          // slow down
          minPosRate.z = -0.1;
          maxPosRate.z = +0.1;
      }
      else if ((Math.abs(dPos.x) < 0.05) && (Math.abs(dPos.y) < 0.05))
      {
          // lateral position correct, use maximum approaching speed
          minPosRate.z = -0.2;
          maxPosRate.z = +0.2;
      }
      targetPosRate.clamp(minPosRate, maxPosRate);
      let dPosRate = targetPosRate.clone().sub(posRate);
          
      // bang control to reach target roll pitch and yaw rate
      if (dRpyRate.x < -dRpyRateControlThreshold.x)
          rollRight();
      else if (dRpyRate.x > +dRpyRateControlThreshold.x)
          rollLeft();
      if (dRpyRate.y < -dRpyRateControlThreshold.y)
          pitchDown();
      else if (dRpyRate.y > +dRpyRateControlThreshold.y)
          pitchUp();
      if (dRpyRate.z < -dRpyRateControlThreshold.z)
          yawRight();
      else if (dRpyRate.z > +dRpyRateControlThreshold.z)
          yawLeft();
  
      // bang control to reach target motion
      if (dPosRate.x < -dPosRateControlThreshold.x)
          translateLeft();
      else if (dPosRate.x > +dPosRateControlThreshold.x)
          translateRight();
      if (dPosRate.y < -dPosRateControlThreshold.y)
          translateDown();
      else if (dPosRate.y > +dPosRateControlThreshold.y)
          translateUp();
      if (dPosRate.z < -dPosRateControlThreshold.z)
          translateForward();
      else if (dPosRate.z > +dPosRateControlThreshold.z)
          translateBackward();
  
  },100);


From all the auto-pilots I've seen so far, I believe mine docks the fastest (without cheating):

https://youtu.be/jWQQH2_UGLw

Source code:

https://gist.github.com/ggerganov/092b86a59fa34926998953701a...


Nice! It will will the award in speed, but not in the amount of fuel it wasted :D


Haha true :) Btw, found this clip of another auto-pilot which is even faster, so my statement above is now incorrect :-)

https://www.youtube.com/watch?v=WwWccjAD2i8


Not to say this is bad or incorrect, but this is very unidiomatic Clojure. I would question the amount of global state, and the broad use of loops inside threads. This can’t be fun to try and test!

Perhaps better would be to make pure functions to take current telemetry as an argument and return actions as outputs, perhaps all at once or more likely individually for each control. You could then capture all of this inside core.async go-loops to manage the control flow and timeouts, but you could also probably build something simple with one or more agents wrapping the current state and updating a set of commands to be executed later. Another argument for core.async would be to make it easier to funnel all commands into a single channel because I’m not entirely convinced a WebDriver is multithreaded, but perhaps the library manages that for you.

Anyway, cool stuff!


Each time someone complains a working program is not "idiomatic" in what ever language I die a little inside.

That makes it sound you approach this from a haughty presumptuous, uncharitable angle.

It sounds like you exclude the possibility that the author did not try it "your way" first, for example, but found a simpler way to implement it.

"That's not the most obvious to do it IMO" or "I would have preferred to do it like this..." are much better ways to approach this. Programs should be beautiful and understandable, but "idiomatic" sounds it excludes lots of simple beautiful solutions to a problem out of pure dogma.


Clojure is a mostly pure functional language, it is very uncommon to have almost all logic reach out to global variables to do work. It’s also rare to see multithreaded code that eschews most of the mechanisms built into the language for multithreading. These two things together (pure functional programming and strong concurrency primitives) are arguably the fundamental motivations behind the language. You value simplicity: Clojure can help make this code simpler. If there’s a succinct way to say that without the word idiomatic, one with fewer negative connotations, I will try to use that in the future.


And those are valid critique. I'm sorry - my prior response was harsh now I'm reading it again.

We all like to critizice things that offend our sense of aesthetics. But criticism always hurts the author. It's ok if it hurts if it is valid critique. And the only way to get understood is to be explicit.


I'm a Clojure fan but I'm not sure a modeling and simulation problem such as this is the best use case for it. Sure, you can do it, but it probably wouldn't be my first choice. Last I checked, Aerospace software is still dominated by C/C++ and Ada (still alive because it found itself a niche), and there's good reasons for it. The satellite simulators I worked on had to be able to execute actual flight code from the vehicle, so you needed to get fairly low level.

Nowadays I write web applications like everyone else on the planet and use Java, JS and/or Clojure (when I can get away with it).

But cool little project, nonetheless.


I would have to google but I’m fairly sure Rich Hickey has expressly state’s this sort of project as a motivation for Clojure. Perhaps not with such real-time demands or For integrating with hardware, but certainly from the exploration and theory side.


Back when I worked on spacecraft simulation we used Ada, and we liked it (no, we didn't).



For some reason I expected this to just interact with some form of http API. After digging into the source code I discovered the author is using etaoin. This is a really neat use of the etaoin library for selenium bindings in Clojure.


Really nice. Made it right up until almost docked.

It crashed for me right as it docked with:

    Syntax error (ExceptionInfo) compiling at (/private/var/folders/8f/wlmmp1rs38966nkmrs_slnjd1vkfkk/T/form- 
    init8037093313867620508.clj:1:125).
    throw+: {:type :etaoin/timeout, :message "Wait for {:css \"#success > h2\"} element is visible", :timeout 300, :interval 0.33, :times 910, :predicate #object[etaoin.api$wait_visible$fn__4868 0x173b24c4 "etaoin.api$wait_visible$fn__4868@173b24c4"]}


If you want the reverse of an autopilot, try this:

https://news.ycombinator.com/item?id=23172281

... and pull the appropriate Interstellar music up in a separate tab.


OK so yes the ISS docking sim is old news, but good lord do I love that UX. I really hope someone has modded it into Kerbal Space Program!


It's definitely easier to read than KSP's navigation ball.

KSP was also 100% the reason I managed dock with the simulator on the first try, because in a way it was more like the 50th-100th try, just using a different interface.


That is some nice looking code!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: