Juno: New Origins
Недостаточно оценок
A Vizzy toolkit for orbital mechanics
От mreed2
This guide documents a craft file which provides a set of tools for implementing a range of orbital maneuvers in Vizzy, including:
• Increasing or decreasing apoapsis or periapsis.
• Aligning planes between the active craft and another object.
• Aligning apse lines
• Adjusting the apoapsis of an orbit so that the active craft and target object reach periapsis at the same time.
• Adjusting Apoapsis, Periapsis, and apse lines at the same time (sometimes)
• Rendezvous between two co-planar objects that do not share a common apse line
• Rendezvous between two non-co-planar objects
• Patched orbital conics (aka “Detect when the current orbit intersects with another sphere of influence and determine what the orbit will be in the new sphere of influence”)
• Ballistic impact prediction

This guide does not explain why or how these functions and instructions work (I have a different guide for that), just how to invoke them.
2
   
Наградить
В избранное
В избранном
Удалить
Introduction
This guide provides documents how to use the various user defined functions and user defined instructions implemented in this craft file to perform a wide range of orbital maneuvers. It very intentionally does not try to explain why these things work – that is the subject of another guide.

The goal is to convert literal rocket science into something that is usable without spending weeks learning the underlying math. I do not know how well that I achieved this goal, but… We will see, I suppose.

This guide does not attempt to explain, or even show, how and why things work -- it just asserts that it does, and here is how you can invoke them to do useful things.

If you do want to know the "why" and "how", I've written another guide:

https://steamcommunity.com/sharedfiles/filedetails/?id=2961230801
Change Log
When significant changes are made to this guide, this section will be updated to describe the changes.
Craft file
This guide is meant to document the Vizzy code attached to this craft file:

https://www.simplerockets.com/c/EJskR8/Orbital-Mechanics-Toolkit
Audience
This guide is intended for people who are familiar with Vizzy and / or programming in general who wish to perform automated orbital maneuvers but do not want to learn tons of math. In particular, you do not need to know about vectors, trigonometry, calculus, or orbital mechanics beyond what you can learn via experimentation with Juno without automation.
Features
As a result of a good deal of time I have put together a series of user defined functions and user defined instructions in Juno: New Origins that allow the manipulation of orbits in wide range of ways:
  • Increasing or decreasing apoapsis or periapsis.
  • Aligning planes between the active craft and another object.
  • Aligning apse lines
  • Adjusting the apoapsis of an orbit so that the active craft and target object reach periapsis at the same time.
  • Adjusting apoapsis, periapsis, and apse lines at the same time (sometimes)
  • Rendezvous between two co-planar objects that do not share a common apse line
  • Rendezvous between two non-co-planar objects
  • Patched orbital conics (aka “Detect when the current orbit intersects with another sphere of influence and determine what the orbit will be in the new sphere of influence”)
  • Ballistic impact prediction
Interplanetary transfers are not included, sorry. Partly I just ran out of enthusiasm and partly interplanetary transfers are really, really hard.

Additionally, it does not include:
  • Launch to rendezvous
  • Landings
  • Automated docking
These omissions are deliberate and are not likely to change. The focus of this project is orbital mechanics, and none of these tasks have much to do with orbital mechanics.

It does include a very simple gravity turn script, but this is not a very good launch script even by my own standards and is simply included to simplify testing.

It also does not include any sort of user interface for two reasons:
  1. I conceived of this as a programming toolbox for other users, rather than as a MechJeb replacement. Thus, a UI is not required.
  2. Given the limits on input / output from Vizzy, it would be very difficult to make a good user interface. Yes, you could use a MFD to create a nice interface, but there is no way to place the contents of a MFD in a window so it would only be usable from a 1st person perspective which is unacceptable in my opinion.
If you are looking for a MechJeb replacement, I would suggest this[www.simplerockets.com] craft file. While the implementation is much less robust than mine (it does what it does, but extending it would be all but impossible), it does include a simple UI to automate the most critical orbital tasks. It is also much smaller and thus much more mobile friendly.

Alternatively, it would not be especially difficult to add a simple, action group based, UI to the current code.
Steam guide rant
Steam guides are very handy in most respects. Sure, it would be really nice if the image tag had an option for "original size, but don't allow any text to be placed on the same line" and "center this horizontally," but if you are looking for a way to publish a text based guide or walk-through it works quite well.

Except...

There is a limit of on how many characters can be put into one section. This is 8,000 characters.

So... Yeah, that's the explanation as to why there is an "Orbital Basics (1/3)." Very annoying, but I suspect that it is inherited from other places where the limit makes much more sense (review and posts have the same 8000 character limit).
Bug disclaimer
I am 100% confident that this code contains bugs. In particular, both of the phasing custom instructions are... Flaky. There are a lot of corner cases that need to be tested, and it seems, based on my experience to date, that every single corner case points out an error in my current implementation.

If you find a bug, feel free to report it here on the Steam guide (preferred) or in the comments thread on the Juno: New Origins craft file (less preferred). However, unless you can provide detailed steps to reproduce, it is unlikely in the extreme that I will be able to reproduce, much less fix, the error. I am also very uncertain how much time I want to continue to invest in this project, so… My recommendation would be to fix it yourself, then upload a different craft file with the fixed code.

Keep in mind that the bugs being discussed here refer to errors in the code in the "Orbital Maneuvers" sections, rather than bugs in the underlying orbital mechanics equations. I'm highly confident that the functions and instructions defined in the "Orbital Basics" are correct. Putting those instructions together to produce useful orbital maneuvers, however, is much more difficult.
Vizzy disclaimer #1
There are certain features that I have found do not work as I expect in Vizzy, and therefore intentionally avoided in all or most cases. These include:
  • The equality block (“[] = []”) does not work as I expect with string inputs. While awkward in many respects, I use the “[] contains []” block instead, which does work how I expect, although I suspect that it is much slower.
  • For loops sometimes do not work as I expect. This seems to be caused by nesting for loops and using variables for the upper / lower / increment parameters of the for statement. However, it does not always happen, and even within a single set of nested for loops sometimes the inner loop works as I intended it to and sometimes it does not.
  • The Vizzy “Increment By” block. I tested this quite some time ago, and it is possible that it works as I expect now, but at the time that I tested it only worked as I expected only occasionally. Here it seemed more likely to fail when the amount to be incremented was a variable.
Pursuant to a previous comment by a developer, I assume that these are working as designed, not a bug, and would be hard to change to boot and have not, nor will I, report these as “bugs.” I see no reason to invest the time and energy in identifying exactly when these features do not work as I expect and submitting a bug report only to find out that these blocks are working as intended.

Thus, in several places where you might expect for loops to be used you will find while loops, and you will find many, many examples of set “X to X + [value]” rather than “Increment X by [value].” If this bothers you, feel free to fix it. If you feel that this should be reported as a bug, feel free to do so, although you should verify that these operators are not working for yourself, so you have sample code to provide. Comments on this topic will be ignored on the craft file and deleted and ignored on the Steam guide.
Vizzy disclaimer #2
The code makes extensive use of an undocumented feature of Vizzy to create variable dynamically (at run-time, without using the green “Create Variable” button). You can read more about how this works in another guide that I have put together, here:
https://steamcommunity.com/sharedfiles/filedetails/?id=3033402620
Since this is not a documented feature of Vizzy there is no guarantee that it will work as expected for all versions on all platforms. If it breaks, this code will break into a million pieces, so… If you try this code and it does not seem to work at all, then this is the thing that I would investigate first.
Missing Vizzy features
There are several features that are missing in Vizzy that made this project far, far harder than it should be. In order of criticality:
  1. Inability to edit the names or parameter lists of user defined functions or user defined instructions after creation. This makes it insanely difficult to impose a consistent naming convention on organically grown code as you can see form this guide. The only current mechanism for performing this operation requires:
    1. First, locate all invocations of the user defined function / instruction. For each, split the code so that you can delete the invocation.
    2. Delete the existing user defined function / instruction definition.
    3. Recreate the user defined function / instruction definition.
    4. Re-add the invocation calls that you deleted earlier.
    At best, this is highly tedious – more commonly, this is highly likely to result in the introduction of bugs when you fail to get the parameter lists for the restored invocations exactly correct.

    As a result of this missing feature, you will find that the variable lists of many instructions / functions use different naming conventions. Everything works, but sometimes an orbit is referred to as "Orbit," sometimes "in_Prefix_Str," and sometimes as "Orbit_Str." This is much less than ideal, but fixing it is far, far, far too much trouble to reasonably consider.
    The same functionality should exist for global variables – but this is of much lower priority as such variables can be safely renamed by saving the craft, exiting the game, opening the XML file for the craft, and perform a search and replace operation replacing the incorrect name with the correct name. Such a simple method is not feasible for custom instructions or functions as each invocation includes the lists of parameter names.
  2. The ability to exit a user defined instruction prior to the end of the instruction definition. Having to put “if not <condition that indicates an error> then <90% of the user defined instruction>” is very hard to troubleshoot, especially since you need to detach code to delete unwanted lines, which means that it becomes very easy to reinsert the removed block at the wrong level of indention, which creates errors that are very painful to troubleshoot.

    As a result of this I largely avoided error detection -- and, when the code does detect an error, it simply logs it and proceeds, perhaps producing incorrect burns and / or falling into an infinite loop.
  3. Local variables. Oh my god, local variables. I think it is obvious why this would be highly useful from looking at the variable list for this project.

    As a result of this missing feature, variable names are absurdly long, which leads to absurdly long Vizzy lines.
  4. A “Snap To Grid” option is desperately needed in Vizzy. Lining up code blocks in columns (as seen in this project) is an incredibly tedious process and it should not be.
  5. The middle mouse button should be for scrolling (clicking and dragging to move the point of view) only. Currently, it acts the same way as the left mouse button, scrolling if there is no code under the mouse cursor but otherwise picking up the code under the mouse cursor. The inability to scroll without accidentally grabbing blocks of code has created numerous very hard to troubleshoot bugs.
  6. Some way to organize / hide user defined instructions / functions / variables. Most of the user defined instructions / functions are meant to be used by the ultimate end user – but some are really meant for internal use only. Currently, the instructions that are only meant to be used in one, very specific, context cannot be separated from the ones that are meant for more general use, and this makes it hard to find the instruction that you are looking for.
Performance disclaimer
I strongly suspect that it will be very difficult to view or edit the Vizzy code on a mobile device. This is a very, very complex Vizzy program (the total craft file size is ~1 MB, and almost all of that is Vizzy code) and I experience noticeable slow down (~5 seconds) when opening the Vizzy code for editing on my desktop PC. I would not be surprised if a mobile device that is otherwise capable of running this game takes 2-3 minutes to open the code for editing, and 10-15 seconds to scroll or change zoom levels.

This is largely out of my hands – feel free to provide feedback to the developers if you feel the performance is too slow.

I do expect that the code will run at an acceptable rate on mobile devices (although the impact prediction logic might be too slow to be usable in a dynamic situation – for example, performing a suicide burn or running a PID to guide the craft to a specified landing site). My major concern is performance of the Vizzy editor.
Accuracy disclaimer
Tl;dr: When time compression is set to 1x or 2x, the formulas used in this guide will not produce predictions that match the values the game produces. The underlying problem is that the game is wrong, but it is doing as well as can be expected given the limitations it is forced to exist under.

The base code (the stuff that deals with orbital mechanics) is quite well tested, and I can say with some degree of confidence that it always produces 100% correct results. The issues with bugs described earlier refer to the code that stacks on top of this basic code – that code is, oddly enough, much more complex and much harder to test than the basic orbital mechanics math code.

However…

This game, along with KSP (both 1 and 2) and Orbiter (a free realistic space flight simulator, available here[orbit.medphys.ucl.ac.uk]) uses a numerical force integration model at low levels of time compression. Numerical integration and analytical integration differ from one another in several respects:
  1. Numerical Integration: In this mode, the game:
    1. Sum up all the forces acting on an object, with the most common examples being gravity, thrust, drag, and lift,
    2. Divides the net force by the mass of the craft to produce an acceleration vector,
    3. Multiplies the acceleration vector by a small increment of time, which I will call a “physics frame tick,”
    4. Adds the vector from the previous step to the current velocity vector,
    5. Multiply the new velocity by the “physics frame tick” length,
    6. Add the vector from the previous step to the current position vector.
  2. Analytical Integration: In this mode, the formulas used in this guide are used to produce the new velocity and position vectors after the passage of an arbitrary amount of time. Computationally, it costs the same amount to produce new velocity and position vectors 0.01 seconds in the future as it does to produce a prediction 1,000 years in the future. Certain operations take much longer (specifically, going from time to position or velocity is much more expensive than the reverse), but even then, the cost does not scale with the increment at all. This is commonly referred to as being “on rails,” and is how these games support high levels of time compression.
In Juno: New Origins, the numerical integrator is used at the 1x and 2x time compression, with all higher time compressions using the analytical integrator. Other programs may have different behavior, and some allow you to configure when the switch occurs.

Numerical integration is, fundamentally, inaccurate. It assumes that both the force (both magnitude and direction) acting on an object and the mass of the object will remain constant over some small period of time (in Juno, on my computer, typically ~0.0167 seconds). This is not an accurate assumption (the mass will drop as fuel is burned, the distance and direction of the gravity force vector will change as the craft moves) and thus errors are introduced in the position and velocity of the craft. As an analogy, the game is approximating drawing a circle by drawing a 5000-sided polygon. Yes, a person looking at the result would not be able to tell the difference, but… If you look deep enough, some of the pixels that are part of the 5000-sided polygon would not appear on the circle and vice versa.

Why use numerical integration if it is so bad? Easy – there is no known analytical solution when more than one force is acting on an object at a time. And “two forces” includes “The engine is firing while gravity applies to the craft.” Thus, numerical integration is required when multiple forces are involved due to lack of an alterntive. If you feel otherwise, feel free to work on the problem – if you succeed, you will get millions of dollars and likely several different international prizes, so go for it! 😊

Although now that I think about it some more, if you do solve this problem, you should probably makes sure you publish your results in a very public way before you tell anyone about your accomplishment. I suspect solving this problem would also prove P=NP which will break the vast bulk of cryptography. If word got out that you had solved the problem then you might have men in black from world governments looking to ensure that they acquire your solution -- and that nobody else does.
How inaccurate is it?
I wrote some code to monitor a craft in a stable, relatively low altitude (100 km x 102 km) orbit, and these were the results:

At the start of the experiment, the game reports the following orbital elements:

When my monitor started, the first thing it did was independently calculate the same orbital parameters producing this:

As expected, my independently calculated orbital parameters exactly match the values returned by the game.

Then, the monitor compares the calculated velocity and position versus the actual (game reported) velocity and position in two reference frames – the PCI reference frame, which is the default for the game, and my perifocal reference frame, which will be described in the next section. This produces the following results:

Annoyingly (to me), the numbers do not exactly match, even though supposedly no game time has passed. I suspect that the issue is that the physics tick occurred when between when I captured the timestamp and when I captured the game values for position and velocity, but I cannot prove that this happened. See below for more on this, however.

I allowed the simulation to run at a 1x speed for several hours (2 hours 7 minutes, to be exact), periodically capturing information to the log. The last few entries look like this:

The position error has grown large (~2.2 km) and velocity errors are notable (~2.60 m/s). This is enough to noticeably change the orbital parameters, which the game recognizes:

Another, much easier, way to detect there is a problem with numerical integration is to simply look at the “Argument of Periapsis” value as returned by the game (as shown in the previous screenshot). During numerical integration, this number will change at a notable rate (~0.01° / second), which should not happen. Only three values are expected to vary in the “Orbit Details” info panel if the only force acting on the craft is gravity – “True Anomaly,” “Time to Apo” and “Time to Per.” All the other variables are parameters that describe the orbit and should remain constant.
Juno rebuilds the orbital parameters of any craft using numerical integration with each physics tick. This explains both why the true anomaly values reported by the game differ from the values I calculate and why the game reported “argument of periapsis” value changes steadily as time passes.
I also suspect that there is some sort of hack going on in the numerical integration code to ensure that the orbital plane does not change due to errors in numerical integration. It is very, very odd that the Z component of perifocal position (which measures “out of plane” position or velocity) never varies very far from the expected (0) value of the Z component. Whether there really is something along these lines or it is simply a very nice side-effect of how errors are introduced by numerical integration, it is certainly a good thing.
What can you do about this? There is only one solution – do not allow the numeric integrator to run at all. You can achieve this by setting the time warp to a high enough value (in Juno, 10x) to force the use of the analytical integrator immediately after each burn, and leaving it at that setting (or higher) until the next burn. You do have to drop to 1x before the burn (to adjust the craft’s attitude to the burn’s attitude) and you need to leave it at 1x during the burn, but at the end of the burn reset the time warp before you calculate the new orbit. If you do this, the same monitor code that I used previously reports this:

As can be clearly seen, there is no error at all between the calculated and actual position or velocity. Thus, errors in the first example must be caused by the numerical integration rather than errors in the code that I wrote.
This also explains why I am confident that the errors in the T=0 screenshot in the first example are caused by an unwanted physics tick – if it were a legitimate error, then the error would be reproduced in the above example, but no such error exists.
The downside of this is that you have less time real time to perform calculations between burns, but… I consider this a fair trade. Still, your mileage may vary.

Another, vastly inferior in my opinion, is to recalculate your maneuver continuously until it is time to begin the burn. I do not think it would be possible to split your “Calculate maneuver” code from your “Perform burn” code, in this case, but… It is not required that these be separate entities.

When the orbit is highly elliptical and the craft is near apoapsis, the errors introduced by numerical integration are significantly smaller. This is a very good thing, as it makes rendezvous with moons more accurate than you might expect. For the following example, I took a craft in a 100 km x 102 km orbit, calculated, and executed a series of burns to transfer to Luna. The orbital parameters after the transfer burn were:
I entered 10x time warp immediately after completing the burn to enter the below orbit, and I calculated the orbital parameters immediately after I entered 10x time warp. This ensured that both the game and my code were 100% in sync until I dropped out of time warp much later, immediately before the SOI transition.

Using that data, plus the orbit of Luna, I determined that I would enter the SOI of Luna and transition to the following orbit:

I dropped out of time warp 5 minutes before the expected SOI transfer, and started monitoring my calculated position and velocity versus the actual position and velocity:


The error in the position of Luna is always zero. This is expected – planets are always “on rails.”

The top-most entry in the screenshot above was triggered by the actual SOI transition occurring, and shows the values for the physics tick immediately preceding the SOI transition occurred.

The new orbit, based on the game’s reported position and velocity turns out to be:

This differs slightly from what I pre-calculated, but it is close enough for most purposes.
For the purposes of demonstration and testing, I intentionally dropped out of the time warp 5 minutes before the transition. There is no need to do this, of course – you could drop out 2 seconds before and the delta between the expected and actual orbits will drop below the limits of what I am choosing to display. In fact, in Juno there is no need to drop out of time warp for SOI transitions at all, so you could just not worry about this at all.
The perifocal reference frame
I assume here that you are aware of what a reference frame is and how you convert vectors from one to another. If this is not true, please refer to my other guide:
https://steamcommunity.com/sharedfiles/filedetails/?id=2944674093
Especially the section entitled “Redefining axes for fun and profit.”

The perifocal reference frame looks like this (for an elliptical orbit):

It is important to note that the planet does not lie at the center of the ellipse but instead one of the two focal points (the “occupied focal point”). By design, the occupied focal point will always be the one on the right, placing periapsis on the right-hand side as well.
The unit vectors are defined as follows:
  • +X = the direction from the center of the planet to the position of the object at periapsis. This is the same direction as the PCI eccentricity vector (e), which is one of the fundamental orbital parameters that will be calculated later. The line that connects periapsis and apoapsis is sometimes called the “apse line.”
  • +Z (not shown) = one of the two vectors perpendicular (aka “normal”) to the orbital plane. This is the same direction as the PCI angular momentum vector (h), which is one of the fundamental orbital parameters that will be calculated later.
    Due to the fact that Unity games use a left-handed definition of the cross product, and decisions that I made in the calculation of h, this is the inverted (multiplied by -1) h, rather than just h.
  • +Y = the cross product between the +X and +Z vectors. This will be the vector that is tangent to the orbital path at periapsis.
The perifocal reference frame has a wide range of highly desirable characteristics that make its use all but mandatory when performing orbital mechanics:
  1. First and foremost, all orbital motion will occur within the perifocal X / Y plane – practically speaking, the Z component can be ignored altogether, turning a complex 3-dimensional problem into a much more tractable 2-dimensional problem.
  2. Second, regardless of whether the craft is in a retrograde or a prograde orbit, the craft always moves in a counter-clockwise direction in the perifocal reference frame.
Finally, the position of the craft can be very easily converted into a polar form, which is even easier to work with than the Cartesian version of the perifocal reference frame. To do this, we describe the craft’s position in terms of an angle with the X axis (also known as the “Line of the Apses”) and a distance from the center of the planet. This looks like this:

The angle θ is assigned the name “True Anomaly,” and “r” is the magnitude of the perifocal Cartesian craft position vector.

Converting to a polar coordinate system has one additional benefit: we only need worry about one value in 99.99% of the cases, specifically true anomaly. Most of the stored instructions used to handle orbital mechanics uses true anomaly as a proxy for “The position of the craft.”

Additionally, note that the craft’s true anomaly value will always be 0 radians (0°) at periapsis, and π radians (180°) at apoapsis. Finally, true anomaly will always increase as the craft moves along its orbit, even if the craft’s orbit is retrograde.
Circular orbits (eccentricity = 0)
This code cannot handle perfectly circular orbits (where eccentricity = 0). For such orbits, the eccentricity vector (e) will have magnitude 0, and a zero vector has no direction. Or, if you prefer, a zero vector points in all directions at the same time.

Almost circular orbits (say, with an eccentricity of 0.00001, where the apoapsis and periapsis differ by 0.1 meters) are fine. Issues only come up with perfectly circular orbits, which simply do not occur without resorting to the debug menu (or, perhaps, extremely bad luck).

There are some checks to prevent circular orbits from occurring, specifically:
  • If you try to create an orbit where apoapsis = periapsis, a warning message will be written to the log and the apoapsis will be increased by 0.01 meters.
  • If you try to create an orbit from a state vector, and the calculated e vector is 0, then an error will be displayed. The code will continue to run, but nothing will work correctly.

It is, of course, possible to modify the code to work properly with circular orbits. However, this requires quite a bit of special case code throughout the project, and I am not willing to the spend this level of time on something that will only occur if someone goes far, far out of their way to create the problem.

Parabolic orbits (eccentricity = 1) are much less problematic, and explicit support is include for orbits of this type.
The "Landing Zone"
When you open the Vizzy editor, you will see something that looks like this:


The exact code you see may vary if I end up updating the craft file, and obviously this will be affected by the resolution and the UI size settings you have set.

For the sake of completeness, I'll go over this code briefly:


This stalls for a single physics tick (to ensure that everything has loaded), then initializes a wide range of variables required for the orbital mechanics code to work properly, then waits for a specified number of seconds.
The purpose of this wait statement is to make it easy to setup desired conditions for the next run of the craft. It isn't required in operational use, of course.
Time compression is reset to "Normal" (as, by default, time compression will be set to 10x after the "Wait x seconds" returns) so that you can stage the craft to launch it.



This code continuously recalculates the orbital parameters of the active craft, determines when / if the orbit impacts the planet, then makes sure that the "Put a custom target reticule on the main view" code has what it needs to do its thing.
This is all optional, but it serves as a decent "health check" of the code. If the estimated impact location isn't directly below the craft when it is setting on the pad, something is wrong.


This code ensures that stage 3 is ditched. Almost always, the auto-launch will exhaust stage 3 prior to achieving orbit, but not always.
Note that the default automatic burn logic cannot handle a staging event during a burn. In most cases it will immediately terminate the burn if the current stage runs out of fuel, but it can have other behavior as well. Dealing with staging during a burn would require additional special case code and would never work particularly well, so I do not attempt to deal with it.


This, as you might expect, disables the RCS jets on the craft (if any). Using RCS jets for attitude control will lead to significant errors in burns, so I turn them off (the auto launch logic turns them off at the start of the launch and on at the end).



This blocks my auto-staging thread from, well, auto-staging. This is another "I'm quite positive that this is working as designed and would be hard to change" things -- sometimes when the time warp is changed from x10 to x1 there will be a physics frame tick where the "Max Engine Thrust" is set to 0 when I do not expect it to be. Since the auto staging logic checks for "Max Engine Thrust" = 0 to determine when autostaging should occur... Bad things occur.
This can probably be worked around by simply waiting for the next physics tick and verifying that "Max Engine Thrust" is still zero. Given that I do not need autostaging after launch, I didn't worry about it once I realized what the problem was.

This is where you should place the actual "Do Orbital Mechanics stuff." In this case, it performs a (non-coplanar) transfer to the moon T.T.


An alternative block is floating around -- this code performs a "slow" rendezvous with the craft named (currently "Medium Price Lander with 1 Telescope," which you will obviously need to change to a craft that exists in your game).

Other floating blocks (below the visible screen area) setup transfer to the other moons of Drool, and provide templates for other sorts of rendezvous with craft.


This block performs two cleanup tasks -- re-enabling the RCS jets (if they exist) and resetting time compression to 1x.

It then goes into an infinite loop where it compares the calculated position and velocity of the craft against the game reported position and velocity. This allows easy reproduction of the testing carried out in the "How inaccurate is it?" section.
Obviously, the monitor is not required for operational use. If you actually perform burns while the monitor is running it will not detect that you made a burn and will continue to show deltas against the original orbit (which, of course, will be very, very large).
Code overview
A very simplified diagram of where code resides in this massive Vizzy project:

A brief description of the various code blocks:
  1. “Perform Burn”: All the logic that performs burns lives here. It expects a true anomaly and a PCI Delta-V vector as input and, well, performs burns.
  2. “Orbit and Vector text formatting”: This code is responsible for formatting vectors and orbits for display. It is essential for troubleshooting, but not required from a functional standpoint.
  3. “Orbital Maneuvers #2”: This is more advanced single impulse (one burn) implementations of various orbital maneuvers you can perform. While there are some intermediate (not meant to be called from outside other user defined instructions in this group) user defined instructions, the bulk return a true anomaly value (in “Burn_TA”) and a PCI Delta-V vector (in “Burn_PCI_DeltaV”) that are ready to be passed to the “Perform Burn” block to perform the burn.
  4. “Orbital Maneuvers #1”: These are the more basic single impulse implementations of orbital maneuvers. They work exactly the same as the user defined instructions in the previous point, returning “Burn_TA” and “Burn_PCI_DeltaV.”
  5. “Impact Prediction”: The impact prediction user defined instruction lives here. In addition, a small helper thread exists, which allows the target reticle to be placed on the impact point.
  6. “Patched Conics #1”: The code responsible for detecting entering a child SOI or exiting to a parent SOI resides here.
  7. “Patched Conics #2”: The code responsible for building a fully realized series of patched conics (which may include an unlimited number of different “patches”) resides here.
  8. “Orbital Basics”: The code responsible for implementing the formulas for motion (as developed by Newton and Kepler) reside here. Most of the code here are user-defined functions, and when they are not it is because I could not come up with a way to make them a user-defined function within the limitations of Vizzy.
  9. “Simple Launch To Orbit”: The code responsible for getting the sample craft to orbit. It is not very good even by my own standards, but it works for testing… 😊
  10. “Init & Utilities”: General purpose utilities that might be useful in context beyond orbital mechanics resides here, along with the an “Init” user defined instruction that must be called before anything else is called.
  11. “Compound Orbital Maneuvers”: This contains pre-packaged multi-impulse maneuvers. For example, rendezvous with a target resides here.
  12. “Orbit / SOI Monitor”: These are three custom instructions that are mostly intended for debugging but could also be used to display status information to the user while an autopilot is running.
The remainder of this guide will cover the specific user defined functions / instructions that reside in each of these sections with comments on proper usage.
Init & Utilities (1/2)

This sets a wide range of variables to their correct staring values. It also initializes a range of global variables that control application level options. Unlike the bulk of the rest of the code, there are inline comments here that describe what the effects of the various options are, so you should review the code for more information.


This user defined instructions initializes the strings that will form the rotation matrixes for PCI->Perifocal and Perifocal->PCI coordinate conversions elsewhere in the code. It is called automatically from “Init.”


These two user defined instructions recursively generate a list of all the planets in the currently loaded solar system. This is required because of limitations in dynamic variables (while you can store strings in them, you cannot retrieve strings from them) and my desire to be able to store planet names as an attribute of orbits. The result is a flattened list of planet / moon names that can be referenced by a numerical index. This is also automatically called from “Init.”


This scans through the list of craft’s orbiting the current planet / moon and returns the last craft whose name contains “Partial_Name.” This is a helper function that makes it much easier to automatically set a target for testing purposes. The selected craft’s Vizzy assigned ID is returned in “Craft_ID.”


This function returns the PCI position of the named planet relative to its immediate parent. For reasons that are totally unknown to me, the Vizzy block that returns the position of a planet always returns a position relative to the sun. The same block returns velocity relative to the parent, so this seems like a very odd decision. This become annoying enough to me that I created a helper function to automate this conversion.


This converts a PCI velocity vector into a “Prograde / Normal / Radial” vector for a particular “Orbit” at a specific location “True_Anomaly.” This is mostly useful for debugging, although one of the perform burn termination logic options uses this user-defined function to determine which attribute is “most important” for a burn.

The X component will be the prograde / retrograde component of the velocity, the Y component will be the Normal / Anti-Normal component, and the Z component will be the Radial-In / Radial-Out component of the velocity.
I am not certain that the sign on the Normal / Anti-Normal component is correct. For the purposes that I use this, this sign does not matter, thus I have not tested this.

This converts a vector in cartesian coordinates (x, y, x) into spherical coordinates (θ, φ, r). θ will measure the rotation in the X / Y plane (“Heading”), φ will measure the rotation in the X / Z plane (“Pitch”), and r will be the magnitude of the original vector. This is used, in conjunction with the following user defined functions, to generate heading and pitch values that can be fed into the built-in autopilot that will be correct when the craft reach a specified position.


These three user-defined functions return the “North,” “East,” and “Up” unit vectors for an arbitrary point on the orbit. These are required as inputs to the next user defined function.


This uses the unit vector functions defined previously to convert a PCI vector into a NEU vector, where the X component is the magnitude of the input vector in the “North” direction, the Y component is the magnitude of the input vector in the “East” direction, and the Z component is the magnitude of the input vector in the “Up” direction. The magnitude of the vector will remain unchanged.

It is much easier to generate heading and pitch values (for the built-in autopilot) when a vector is expressed in this reference frame.

Because the unit vector must be re-calculated from scratch each time this function is called, this is much slower than the NEU conversion function presented in my other guide. This is appropriate to call once, for the purposes of getting heading and pitch values to point an orbiting craft at which will be correct at some later point in the orbit, but not for driving a PID controller to hop between two nearby locations on a planet.


This is the cumulation of the last several user defined functions – it sets the built-in autopilot to the heading and pitch values that will correspond with the specified PCI attitude vector when the craft reaches the specified true anomaly value. This allows you to point the craft in the correct direction for a burn before you reach the point in the orbit where the burn will start.


This helper user defined function returns how much time will pass for the object in “Orbit” to travel from the first true anomaly to the second true anomaly. It is smart enough to return the correct results when the craft passes through periapsis (0°) which is the main point.


This user defined function answers the question “Given the craft is current at a particular true anomaly, which true anomaly value will it reach next?” The main advantage of this user defined function is that it properly handles the case where the craft passes through the periapsis before reaching one of the two points.


Returns “true” if “Value” is the string “NaN” (Not a Number). Many Vizzy operations return NaN when invalid inputs are provided (for example, division by zero or arccosine of a number greater than 1 or less than -1), and detecting this is critical in several places.


This simply returns +1 if the value is positive, -1 is the value is negative, or zero if the value is, well, zero. This is highly useful when performing a binary search, as you need to check to see if the direction of the desired value has changed from the previous iteration (for example, you were 500 units away in the last iteration, but now you are -450 units away).


This returns true if "Value_To_Check" equals "Target_Value" +/- "Tolerance" or false otherwise. This is required to compare floating point numbers that come from different formulas (for example, comparing the calculated true anomaly of the craft at its current location vs. Pi) and is caused by the limits of floating point math.

This is also used extensively to determine if a value is "good enough" in the context of converging approximations.


This takes an angle measured in radians in ensures that it falls into the range 0 -- 2π (0 - 360°). This is done partially to improve code readability and sometimes to avoid actual problems.
Init & Utilities (2/2)

If the orbit referred to by “in_prefix_str” is elliptical or circular, this just uses “Normalize Angle Radians” to ensure the angle falls between 0 - 2π (0 - 360°). Otherwise, the orbit must be parabolic or hyperbolic and it ensures the angle falls between -π - +π (-180° - +180°). The later is a much, much, more intuitive way to think about true anomaly values in parabolic / hyperbolic orbits.
This is not required from a math standpoint, and the game itself does not do this – it reports true anomaly values for all orbits in the range of (0 - 360°). This results in a craft on a hyperbolic fly-by trajectory starting with a large true anomaly value (say, 220°) which increases as the craft approaches periapsis, hitting 349.99° immediately before periapsis. At periapsis, however, the true anomaly value drops to 0°, then starts counting up as the craft continues along the hyperbolic trajectory.


This user defined function subtracts two angles, returning the smaller of the two possible results.


This function returns… A vector projected onto a plane as defined by its normal vector. A “normal vector” refers to a vector that is extends directly up or directly down relative to a particular plane, and there will only be one such plane for each normal vector. The caller needs to ensure that “Vector_Normal_To_Plane” is normalized to get the expected results! Also, be aware that if the plane is at right angles to the input vector, then this will return the 0 vector, which is almost certainly not what is desired.


This rotates an input vector (“inVector”) by a certain number of radians around the specified axis. In order to produce the correct results, the caller needs to ensure that the “Axis_Of_Rotation_Vector” is normalized.


These user defined functions implement the hyperbolic trigonometric functions, which are required to calculate time values for hyperbolic orbits. Oddly enough, they are not required in any other context.
The formulas used are not approximations of an infinite series or something similar – these are exact values. For reasons only known to the gods of math, the hyperbolic trigonometric functions have nice, simple definitions in a finite number of terms.


These three user defined instructions provide an alternative to simply using “local log” to generate debugging / warning / error messages – specifically, a mechanism to allow them to be turned off when they are not needed, but turned back on if something goes wrong.


This user defined instruction waits for a specified period of time, using larger amounts of time acceleration when the time to wait is very long.
Orbital Basics (1/4)
This section of code encapsulates the "hard part" of orbital mechanics.
In my opinion, anyone designing a programming API for a realistic space flight simulator should expose all of these functions as part of the API. It is necessary to implement them in order to make such a simulator, so... Why force users to reinvent the wheel?

This is the second most important instruction in this whole project. It is used to convert a PCI position and velocity vectors into orbital parameters, and is most used to generate the parameters of an orbit as it currently exists within the game.

It then performs math and produces the following attributes that define the current orbit:
  1. The ID # (as defined earlier, in “Create List of Planet Names”) associated with the central body of the orbit.
  2. The μ (Greek letter Mu), a value this will be the same for all objects orbiting the same central body.
  3. The PCI angular momentum vector (h) of the new orbit, whose:
    1. Direction is normal (aka perpendicular) to the plane of the orbit, and thus defines the orbital plane.
    2. Magnitude is equal to the total angular momentum of the orbit, which is a constant of the orbit
  4. The PCI eccentricity vector (e) of the new orbit, whose:
    1. Direction points from the center of the orbiting body to the PCI location of periapsis in the new orbit
    2. Magnitude is equal to the eccentricity of the new orbit (for example, 0 for circular orbits, between 0 and 1 for elliptical orbits, 1 for parabolic orbits, and greater than 1 for hyperbolic orbits).
  5. The magnitude of angular momentum and eccentricity PCI vectors are stored as separate scalar numbers, which dramatically simplifies a wide range of math later.
  6. The “Parameter of the orbit” (p) which is also known as the “Latus Rectum” significantly simplifies some of the later math and so is precalculated here.
  7. The “Semi-major axis” (a) simplifies some of the later math and is precalculated here.
  8. If the orbit is elliptical (e < 1), the period is precalculated.
  9. A constant of my own definition (“z”) is defined because it simplifies some of the later equations.
  10. A PCI vector that points from the center of the central body along the line of intersection between the equatorial plane (the PCI plane where the “Y” value is zero) and the current orbit. This makes calculated the additional attributes much easier.
  11. The magnitude of the vector calculated in the previous step. Again, this simplifies the calculation of the extended attributes.
It then goes on to calculate the “additional” orbital parameters (described in a bit – these are extremely useful for using rotations to convert vectors between PCI and perifocal reference frames), if that global flag is set, then initialize the “extended” attributes (of my own creation, which are used to track sphere of influence changes), and finally initialize the perifocal -> PCI and PCI -> perifocal conversion routines (which are orbit specific, but remain constant as long as the orbit does not change).

All of these values are placed into variables whose name starts with "in_Prefix_Str," creating the variables at runtime if required. Thus, after calling this instruction with "in_Prefix_Str" set to "Current" a variable "Current_e" will contain the magnitude of the e vector.


This is the most important user defined instruction in this whole project. It is used to convert apoapsis, periapsis, a PCI vector pointing in the desired direction of h, and a PCI vector pointing in the desired direction of e into the same orbital parameters as the previous user defined instruction. This is primarily used to partially clone an existing orbit while varying certain parameters and is a critical step in almost all orbital maneuvers.
To provide some concrete examples:
  • If you copy all the parameters from your existing orbit except for apoapsis, you end up with an orbit that is identical to the current orbit but has a different apoapsis. The new orbit is guaranteed to intersect with the current orbit at periapsis (which will be the same for both orbits), and a simple comparison of velocity at periapsis for the two orbits will tell you how much delta-v is required.
  • If you copy all the parameters from your existing orbit except for the direction of h, then the resulting orbit will have a different inclination than your starting orbit. While it is more difficult to find where the two orbits intersect, once you do a simple comparison of velocities will tell you how much delta- v is required to change the inclination.
    The new direction of h will be copied from a different existing orbit – the orbit of the target you are attempting to align planes with.
  • If you retain all the parameters from your existing orbit except for the direction of e, then the resulting orbit will be rotated with regards to the original orbit. Finding the points of intersection between these two orbits is more difficult still, but if you do, then a simple comparison of velocities will tell you how much delta-v is required to perform the rotation.
All the above maneuvers will always produce at least one point of intersection between the new and current orbits. Sometimes, it is possible to vary multiple parameters at the same time, which will allow you to combine several maneuvers into one.

The output of this instruction is the exact same as the previous instruction -- a set of variables, all starting with "in_Prefix_Str," that contain the parameters of the requested orbit.


This user defined instruction accepts the name of an orbit as input and calculates three additional orbital parameters:
  • Inclination (relative the equatorial plane)
  • Right ascension of the ascending node (relative to the equatorial plane)
  • The Argument of the Periapsis (relative to the Right Ascension of the ascending node)
These three parameters, all of which are angles measured in radians, can be used to convert vectors from the PCI reference frame to the perifocal reference frame and this is the only purpose they are used for in this code.


This user defined instruction creates an orbit named “in_prefix_str” whose parameters match the active target.

By default, if the target is a planet or moon (detected by checking to see if the name appears in the previous created list of planet names) then it creates a new orbit using the game provided inclination, right ascension, and argument of periapsis values. If it is not a planet, then it checks the “Global_Flag_Use_RA_Inc_And_Arg_To_Calc_Orbit” and if it is set to “true,” it also uses the game provided inclination, right ascension, and argument of periapsis values to create the new orbit. Otherwise, it simply uses the current PCI position and velocity to create the orbit.
Via experimentation, state vectors work best for defining orbits of crafts while inclination, right ascension, and argument of the periapsis work best for planets. This is why the “Global_Flag_Use_RA_Inc_And_Arg_To_Calc_Orbit” is ignored when you calculate orbital parameters for planets.


This user defined instruction does what it says in the name – it generates an orbit from the specified parameters. It is most commonly used to generate orbits from planets, but indirectly, via another function described later.
Orbital Basics (2/4)

These two user defined instructions really should not be used, but are. They work exactly as the previously defined user defined instructions do (and, in fact, call those user defined instructions) except that they assume that the new orbit will be orbiting whichever planet the active craft is currently orbiting.

This assumption eliminates the need for one parameter – at the cost of making the code fail when you are try to use it for a position outside of the current sphere of influence. This is less of a problem than you might expect, because orbital projections across a sphere of influence change are relatively inaccurate and really should not be used as the basis for calculating a maneuver, but…

The main reason that these exist is so that I do not have to rewrite many orbital maneuver instructions to use the newer user defined instructions that explicitly specify the name of the planet being orbited.



This calculates the orbit for a planet directly (without needing to target it first). This is highly useful in checking for SOI changes.

This provides a shorthand to load the orbital parameters of the active craft into an orbit.


This simply calls the previous instruction with a parameter of “Current,” and is the preferred way to update the current orbit after it has changed for some reason (for example, an engine burn).


The perifocal reference frame is all but required to work with orbital mechanics, and this user defined instruction sets up the conversion from PCI to Perifocal and vice versa.

First it defines two sets of unit vectors, one for PCI -> perifocal conversions and one for perifocal -> PCI conversions. Then, if both the “Global_Calculate_Extra_Orbital_Parameters” and “Global_Calculate_Rotation_Matrixes” are set to “true,” it also pre-calculates the rotation matrixes.

The rotation matrixes require quite a few CPU cycles to calculate, but are both more accurate and less CPU intensive when vector conversions are made. Under normal circumstances, the program will perform better when the rotation matrixes are available, but in certain applications (most notably impact prediction) speed may be more important than accuracy.
Keep in mind that “inaccurate” in this context means “After traveling 100000 km the estimated position will be incorrect by 5 meters.” When you are using impact prediction logic you are interested in positions that are maybe a few hundred km away. If the position is wrong by, say, 0.3 meters, that is not likely to be a large issue.

This user defined instruction will pre-calculate the two rotations matrixes required to transform vectors from the PCI reference frame to the perifocal reference frame and vice versa. This user defined function does not respect the “Global_Calculate_Extra_Orbital_Parameters” and “Global_Calculate_Rotation_Matrixes” flags – if you call it directly, it will try to calculate the rotation matrixes.


This converts a perifocal vector to a PCI vector via rotations without using the pre-calculated rotation matrixes. This takes advantage of a clever “hack”: By definition, the perifocal h and perifocal e vectors have trivial values – namely (0, 0, 1) and (1, 0, 0). Thus, if you can convert these two vectors into PCI vectors without first creating the orbit you can produce PCI unit vectors that are correct. Using these two PCI vectors, it is straightforward to create all the orbital parameters.

As this is intended to only be used in this very specific circumstance, there is no “PCI to Perifocal Via Rotations” user defined function.


These convert vectors from PCI to perifocal or vice versa using unit vectors. Outside of debugging, these are not meant to be called directly.


These convert vectors from PCI to perifocal or vice versa using the pre-calculated rotation matrixes. If the rotation matrixes have not been calculated, these will fail catastrophically.


These are the primary means that should be used to convert vectors from perifocal to PCI and vice versa. These use the rotation matrixes if they are available, but use unit vectors when they are not.


These user defined instructions calculate the time from periapsis to a specified true anomaly value for orbits of a given type. They should not be called directly – call the following user defined instruction instead.


This is the user defined instruction that should be used to convert true anomaly to a time since periapsis. It will detect the type of orbit and call one of the three user defined instructions mentioned above as appropriate.

The calculated true anomaly value will be returned in this variable “True_Anomaly_From_t.”

This is a user defined instruction (rather than a user defined function) because it must be – part of the calculation of this conversion requires calculating a value which has no algebraic solution and must therefore be solved iteratively. As a result of this, this is also very slow (0.1 seconds on my computer) in comparison to the other functions and instructions in this section.


This uses the current (as in “Right now”) true anomaly of an object traveling in an orbit to calculate the true anomaly value at some point in the past or the future. This is mostly used to determine where a moon will be at SOI entrance or exit.

This calculates the distance between two objects with starting positions “TA_1” and “TA_2” traveling along “Orbit_1” and “Orbit_2” (respectively) after “Delta_Time” has passed.
This is used in orbital phasing calculations, where the goal is to drive delta_time to a specific value (almost always zero).
As this calls “Calculate True Anomaly From Time From Periapsis” twice, this will also be slow, taking ~0.2 seconds per invocation.


These user defined functions convert true anomaly values into time since periapsis and none of them should be called directly.


These user defined functions convert various anomaly values from one to another and should not be called directly as I am unaware of any purpose of these values except as intermediate stages when calculating time since periapsis from true anomaly.


This user defined function converts a true anomaly value into a time since periapsis, using the previous 14 user defined functions in various combinations.

Calculating this value is relatively fast – while there is quite a bit of math going on, it is all simple algebra.
Orbital Basics (3/4)
This is a helper function that I defined to solve on a highly specific problem that I no longer remember the details of. Sorry. 😊


The value returned by this function can be fed into the arccosine operator to return the true anomaly where the craft reaches a specific radial distance.

It is split out like this because knowing this value is the easiest way to detect whether the craft ever reaches the specified point – specifically:
  • Value > 1: The orbit of the craft is always above the specified distance
  • Value = 1: The orbit of the craft will match the specified distance at periapsis
  • -1 < Value < 1: The orbit of the craft will match the specified distance at two points
  • Value = -1: The orbit of the craft will match the specified distance at apoapsis
  • Value < -1: The orbit of the craft is fully contained within the specified distance
This is used in the impact prediction calculation.


This just takes the arccosine of the value returned by the previous user defined function and finds the distance from the occupied focus (= "center of the planet") at the specified true anomaly value.

It is used in both impact prediction and SOI departure calculations.


This returns the radial distance (r in polar form) of the craft at a specified true anomaly value. This is mostly used as an input to calculating the perifocal position of an object, although it is also used when detecting sphere of influence changes and impact prediction.

There may be two true anomaly values that correspond with the same position – to get the second solution, use the formula “2π – 1st Value.”
If the initial value is 0 then the formula will return 2π (which is equivalent to 0) and if the initial value is π then the formula will return π as well.


These two user defined functions return the periapsis and apoapsis of the specified orbit. Due to the way in which the perifocal reference frame works, periapsis will always be the radial distance at a true anomaly value of 0 and apoapsis will be the radial distance at a true anomaly value of π.

Note that for hyperbolic and parabolic orbits, apoapsis will be... Odd (negative for hyperbolic and NaN for parabolic). These values are correct, and can be used as apoapsis values anywhere in this code where a apoapsis value is required. Well, OK, "NaN" can't be used in all places where an apoapsis would be used. 😊


These two user defined functions return the perifocal velocity and position of the object moving in the orbit “in_Prefix_Str” at the true anomaly “in_TA.” These are used everywhere – if the user defined instructions that setup orbital parameters are the heart of this framework, these two functions represent the muscles.


This calculates the required value of “a” (the semi-major axis) to achieve a specified period (“T”). This is very useful when adjust phasing (where you need to create a new orbit with a specified period to ensure two craft reach the same point at the same time).


This calculates the limit value for periapsis when you rotate the apse line of an orbit a given amount and change the apoapsis value to a specified value. It is called a “limit value” because sometimes the value represents an upper bound (periapsis must be this value or lower) and sometimes a lower bound (periapsis must be this value or higher).

This is something that I came up with for myself, but it turned out not to be very useful, but I spent so much time on it that I cannot bring myself to remove it. If you want to know the details of how to use this function, see my other guide which goes into great depth on how this works.


These values return the specified orbital parameter from the orbit “in_Prefix_Str.” They are used in a wide range of contexts where math is required but it is a “one off” operation, not worthy of getting a separate user defined function.


These return the three extended attributes of orbits, from the pre-calculated values (if this is enabled) or by calculating them from scratch.


These set the specified extended attributes associated with all orbits. As you might expect, these are only defined when the craft exits one sphere of influence for another.


This initializes all the extended attributes to “NaN,” and this user defined instruction is called automatically whenever a new orbit is created.

This is not strictly required, however a Vizzy thread that attempts to access a variable that does not exist the Vizzy thread will crash. Defining all these attributes up front ensures that this does not happen.


These return the expected extended attribute attached to a particular orbit.
Orbital Basics (4/4)

These return intermediate values that are used in the “True Anomaly Of Points Of Intersection” calculation. While these potentially have some value in and of themselves (as a method of detecting if two orbits intersect at all and how many points they intersect at) I do not use them except as intermediate values.


This is another intermediate value used in finding the points of intersection between two orbits. This particular values is highly significant, because if it is outside the range of [-1...+1], then there will certainly be no points of intersection between two orbits.

The problem is, and the reason that I split it out, is that due to floating point errors this value is not quite correct. Sometimes it will be slightly outside of the [-1...+1] range, but the orbits do intersect. By splitting it out, I can easily check it separately and easily output the value for troubleshooting.


This is the same value as above, but rounded to a specified number of digits of precision. This currently is not being used -- I created it with notion of using it as an alternative when the previous function returns an out of range value. I decided against implementing it because the errors are too large to reasonably be ignored (I would need to round to 2 significant digits to get the correct results, and that's far too much rounding).


These return the true anomaly values where two co-planar orbits intersect. This is used exclusively in the context of intersections after an apse rotation, and appears (for reasons that I am not clear on) to only work with elliptical orbits. If there is only one point of intersection between the two orbits both functions will return the same value.


This user defined instruction should not be necessary – yet, I cannot find a way to eliminate it. It returns up to two points where the two input orbits intersect. The difference between this instruction and the previous two user defined functions is that it verifies that the orbits intersect at the specified true anomaly values.

The true anomaly values at the intersection points for the initial orbit will be returned in:


The true anomaly values at the intersection points for the final orbit will be returned in:


If no points of intersection are found, both values will be “NaN.”

Additionally, the variable "Orbit_Intersection_Result_Cnt" will contain the number of intersections found. In theory, this could be as high as 6, but no more than 2 intersection points will be returned.
Impact Prediction

This is something of an odd ball user defined function, which is why it sits by itself. There are three parameters:
  1. The first parameter should always be set to “false.” It controls which of the two solutions that is returned from “True Anomaly From Radial Distance” is used, and this implementation pre-dates me fully understanding that true anomaly values always increase with the passage of time. If this is set to “true,” this will find the point where, if you turned time back, you would have emerged from the planet along the current orbit. Not especially useful, I admit. 😊
  2. The radius you wish to detect intersections with. This is a parameter because I can imagine someone wanting to detect when the active craft’s path intersects with the atmosphere rather than the surface of the planet.
  3. “inAdjust_For_Terrain” should generally be set to “true,” but the algorithm is a bit slower in this mode. When this mode is active an extra return variable is generated and the inRadius value is updated with each invocation to reflect the altitude of the last iteration. Over multiple invocations, adding the altitude of impact point to the radius in this way significantly improves the accuracy of the impact prediction.
There are three potentially useful return values:

  • This is how long (in seconds) it will take the active craft to collide with the planet.

  • This is always calculated and is a vector containing the latitude, longitude, and altitude above sea level of the calculated impact point. Latitude and longitude are measured in degrees rather than radians (as that is how the game generates these values). The altitude portion of this vector will always be set to the altitude referenced by “inRadius,” which will normally be 0 meters above sea level. Obviously, the actual terrain at the location is very likely to be something else and this point will be underground.

  • This is only calculated if “inAdjust_For_Terrain” is set to “true,” and sets the Z component to be the altitude of the actual terrain located at the calculated impact point. Calculating the terrain height at a given latitude / longitude position is one of the more expensive Vizzy functions, and thus this calculation can be disabled if performance is more important than accuracy.

    In addition, the altitude that it finds will be used to adjust the radius of the planet during the next iteration (assuming that there is another iteration).

If no impact is detected, all three of the above values will be set to “NaN.”

If no forces other than gravity are acting on the active craft, this user defined instruction can be invoked once and the target location (in LLA coordinates) will remain correct until impact. This would be the case for calculating the point of impact on a body with no atmosphere after the de-orbit burn is completed.

If other forces are acting on the active craft (most commonly, atmospheric drag), the target location will only be valid for a single physics tick. This is because the orbit that this uses as a starting point will also only be valid for one physics tick.

You can, if you wish, re-calculate the orbit then reinvoke this instruction on each physics tick to produce an updated impact location, but be aware that doing so will take up a large portion of the instructions that Vizzy can execute per physics tick. This is where you may want to set “Global_Calculate_Extra_Orbital_Parameters,” “Global_Calculate_Rotation_Matrixes,” and “Global_Use_Rotation_Matrixes” all to false temporarily. While this will make the impact location a bit less accurate, the additional inaccuracy will be measured in fractions of a meter at the distances where impact predication is relevant – but much less time will be spent updating the orbital parameters.

There is a small stand-alone thread located in this region as well, which starts like this:

As the comments describe, this code will:
  1. Wait until the variable “Auto_Update_Target_With_LLA_Variable_Name” is something other than an empty string.
  2. Set the “target node” Vizzy block to the PCI location that corresponds with the variable name stored in the “Auto_Update_Target_With_LLA_Variable_Name” string. It expects to find a vector in the “Latitude / Longitude / Altitude above sea level” format, and will not properly process values stored in a different format.
  3. Each physics tick, execute step #2 again.
  4. Exit the loop when either “Auto_Update_Target_With_LLA_Variable_Name” is an empty string or the name of the active target is something other than “Target.” The later condition means that either the user or other Vizzy code has changed the target to point at a craft, planet, or point of interest, and we assume that they are no longer interested in the impact point in that case.
  5. If the loop exited because “Auto_Update_Target_With_LLA_Variable_Name” was set to an empty string, unset the current target.
  6. In any case, clear the “Auto_Update_Target_With_LLA_Variable_Name” and return to step #1.
To use this code, just set “Auto_Update_Target_With_LLA_Variable_Name” to the string “v:Impact_Point_Rotating_LLA” or the string “v:Impact_Point_Rotating_LLA.” Once this has been done, a targeting reticle which is only visible in the main view point area (not on the map screen) will be placed at the location that corresponds with the string specified in “Auto_Update_Target_With_LLA_Varaible_Name.”

Be aware that this code does not actually calculate an impact point. As a matter of fact, you could use this exact same code to simply place a custom waypoint on the map – just load the latitude / longitude / altitude above sea level that you wish to mark into a variable, then place the name of the variable in “Auto_Update_Target_With_LLA_Variable_Name.” What it does do is update the target location (which needs to be in PCI coordinates) to adjust for the rotation of the planet.

This code also consumes a significant number of cycles, so you may want to avoid enabling it (do not set “Auto_Update_Target_With_LLA_Variable_Name”) if you are running complex code at the same time as your impact prediction code is running.
Orbital Maneuvers #1 (1/2)
Starting with the top and working down, these user defined instructions perform the simpler orbital maneuvers – the ones that only adjust one parameter at a time. All the instructions that calculate burns return these two values:

  • The true anomaly at which the burn should be performed.

  • The amount and direction (in the PCI reference frame) of velocity that needs to be added to complete the maneuver.
All the user defined instructions that calculate maneuvers expect two orbits as their first two inputs, although the names vary.

If the maneuver does not explicitly deal with rendezvous, the first two parameters will be:
  1. The first parameter holds the string for the orbit the active craft is currently in.
  2. The second parameter holds the string where the future orbit (post-burn) will be placed.
If the maneuver does deal with rendezvous, the first three parameters will be:
  1. The first parameter holds the string for the orbit the active craft is currently in.
  2. The second parameter holds the string that describes the current orbit of the target object.
  3. The third parameter holds the string that will be overwritten with the future orbit (post-burn).

These are simply shorthand functions that pass execution to the next user defined instruction after re-arranging the parameters.


This user defined instruction calculates a burn to change either PE or AP (but not both). The parameters work like this:
  • In_Initial_Prefix_Str: The string for the current orbit.
  • In_Final_Prefix_Str: The string that will be loaded with the parameters for the orbit after the burn.
  • In_TA_Of_Burn: In radians, the true anomaly of the burn. The only valid values are 0 radians (= 0°) or π radians (= 180°). Other values will produce invalid results.
  • In_New_Value: The new value for either periapsis or apoapsis. This value will be interpreted according to in_TA_Of_Burn.
This is a bit more complex to implement than you might expect -- when apoapsis is reduced below the current periapsis (or periapsis is increased above the current apoapsis) the direction of the e vector must be reversed in addition to swapping the apoapsis and periapsis values.


This user defined instruction calculates a maneuver to change the inclination of “in_Active_Craft_Prefix” to match the inclination of “in_Passive_Craft_Prefix.” The parameters are:
  • In_Active_Craft_Prefix: The current orbit.
  • In_Passive_Craft_Prefix: The current orbit of the target object.
  • In_Transfer_Orbit_Prefix: The future orbit, after the burn is completed.
  • In_Active_Craft_Current_TA: The current true anomaly of the active craft – this could be used (but is not at the current time) to determine which of the two intersection points between the orbits should be used to perform the burn.
This instruction is implemented differently from all the other instructions in this project. Everywhere else uses this process to calculate maneuvers:
  1. Find the desired orbit.
  2. Find the points of intersection between the desired and current orbit.
  3. Subtract the velocity of the craft in the current orbit at the point of intersection from the velocity of the craft in the desired orbit at the point of intersection.
This instruction instead finds the velocity of the active craft at the point of intersection, then rotates the velocity vector to be in the plane of the desired orbit, then subtracts the two velocities.

This is done to avoid problems in a very specific case (where the desired orbit's inclination is exactly 90° offset from the current orbit and the line of intersection between the two orbital planes is the same as the apse line of both orbits) where it is very hard to calculate the desired orbit. More details on this, if you are interested, can be found in the companion guide.


This user defined instruction calculates a maneuver to rotate the current orbit (to align apse lines) and to change apoapsis and to change periapsis all in a single maneuver. Due to the way the math works, you are not guaranteed to get the requested periapsis value, and I only use the instruction to align apse lines while adjusting apoapsis. The parameters are:
  • In_Active_Craft_Prefix: The current orbit.
  • In_Passive_Craft_Prefix: The current orbit of the target object.
  • In_Transfer_Prefix: The future orbit, after the burn is completed.
  • In_New_AP: The desired new apoapsis. If this is set to 0, then the current apoapsis will be used.
  • In_New_PE: The desired new periapsis. If this is set to 0, then the current periapsis will be used.
When this user defined instruction is invoked with a 0 in the last two parameters, all it will do is align apse lines.
As a corollary to the previous point, changing apse lines while holding apoapsis and periapsis constant requires a radial burn. Radial burns are very, very, very expensive if the eccentricity of the orbit is high (say, greater than 0.01). If at all possible it is best to combine a rotation of the apse lines with a significant change either apoapsis or periapsis (but not both) as this will produce a much more efficient maneuver.

This user defined instruction calculates a maneuver to align the apse line with the ascending / descending nodes (“line of intersection”) and add a specified amount of velocity in the prograde direction (increase apoapsis). The resulting orbit will have its periapsis located at one of the points of intersection between the orbital planes and apoapsis at the other, which can dramatically reduce the cost of a plane change maneuver. On the other hand, you have to spend a large amount of delta-v to increase apoapsis, so this is only cost effective if you were planning on increasing apoapsis anyway (for example, to adjust phasing) or the plane change maneuver would be very, very expensive otherwise.

The parameters are:
  • In_Active_Craft_Prefix: The current orbit.
  • In_Passive_craft_Prefix: The current orbit of the target object.
  • In_Transfer_Prefix: The future orbit, after the burn is completed.
  • In_Delta_V_To_Add: The amount of delta-v that should be added as part of aligning the apse line. This is a simple number, not a vector – so, if you want to add 1000 m/s, then just specify 1000 in this parameter.
The intent of this function is to provide an alternative for very expensive plane change maneuvers – if, for example, you determine that a plane change will cost 4000 m/s to complete, you can invoke this function with, say, 3000 m/s, then try the plane change using the transfer orbit as the current orbit. If the cost of the two maneuvers is less than the cost of performing the plane change directly, then you can perform the plane change in two parts. Note, however, that you will end up in a highly elliptical orbit, which may result in extra delta-v costs in later maneuvers.
I have not tested this user defined instruction extensively – none of the pre-defined multi-impulse maneuvers make use of it.
Orbital Maneuvers #1 (2/2)

This user defined instruction calculates a maneuver to increase the apoapsis of the active craft so that both the active craft and craft will reach the next periapsis after the burn at the same time.

The parameters are:
  • In_Active_Craft_Prefix: The current orbit.
  • In_Passive_Craft_Prefix: The current orbit of the target object.
  • In_Transfer_Orbit_Prefix: The future orbit, after the burn is completed.
  • In_Current_Active_Craft_TA: The true anomaly of the active craft, in terms of its own orbit.
  • In_Passive_Craft_Current_TA: The true anomaly of the passive craft, in terms of its own orbit.
  • Num_Of_Phasing_Orbits
If both the orbital planes are aligned and the periapsis of the active craft's orbit matches the periapsis of the passive craft, this maneuver will create a rendezvous at periapsis after the specified number of orbits has passed (must be at least 1). If you have not aligned the planes, this will almost certainly fail to produce a rendezvous.

If active craft’s periapsis altitude is not the same as the target’s periapsis altitude, then it will make an attempt (not very successfully, but it will try) to adjust the periapsis values to match -- otherwise, the rendezvous will occur, but the craft will be separated by the difference in periapsis values.

This is one of the two phasing instructions that I am very, very dubious about. There are an enormous number of cases that need to be tested, and I ran out of time and enthusiasm to continue to troubleshoot. When it works, it works very well -- with 2 phasing orbits, the only remaining error at rendezvous is the error in periapsis, but the burns to adjust phasing increase this error, so...

This is an area where there is definitely room for improvement.
Orbital Maneuvers #2 (1/2)
These maneuvers are much more complex, commonly adjusting two or even three parameters of an orbit with a single operation. The interfaces are the same – up to three orbits as input, with two variables for output.



This is not meant to be invoked directly from user code, but it calculates a potential transfer orbit between the active and passive objects and determines how far apart the objects after the active craft completes ½ of the transfer orbit. The primary output parameters are “DT_Arrivial_Time_Error_At_Transfer_End” (which contains the error at the end of the transfer orbit in terms of seconds) and “DT_Dist_Between_Craft_At_End_Of_Transfer” (which contains the error of craft positions, in meters).



This calculates a maneuver to directly rendezvous two craft whose orbits are co-planar. The parameters are:
  • Active: The current orbit.
  • Passive: The current orbit of the target object.
  • Transfer: The future orbit, after the burn is completed.
  • Active_Current_TA: The true anomaly of the active craft, in terms of its own orbit.
  • Passive_Current_TA: The true anomaly of the passive object, in terms of its own orbit.
  • Desired_Seperation_At_Point_Of_Intersection: The desired separation, in meters, between the two objects at the completion of the rendezvous. Be aware that this could be a quite large burn, depending on the initial conditions.
This may not work (has not been tested) when the period of the active craft orbit is larger than the period of the passive craft.

This is fairly slow – taking 2-5 seconds to calculate the maneuver on my desktop. It works by locating a point on the current orbit where creating a transfer orbit that connects the current orbit with the target orbit produces a rendezvous at apoapsis of the transfer orbit. It does this via a binary search algorithm, and since each iteration requires calculating the distance between the craft after a certain amount of time has passed each iteration is slow.

Additionally, the binary search algorithm will fail to converge under certain circumstances. This is detected and it automatically retries, but that does not refund the time spent on the failed attempt. For more details about why this occurs see the companion guide.



This needs to be called (once) immediately before using either of the two following instructions. The parameters are:
  • Active: The current orbit
  • Passive: The current orbit of the target object.
  • Active_Current_TA: The true anomaly of the active craft, in terms of its own orbit.
  • Passive_Current_TA: The true anomaly of the passive object, in terms of its own orbit.


This calculates a maneuver to align the apse lines with the line of intersection between the two orbital planes while increasing the apoapsis of the orbit so that the target and active craft will reach the same point in both time and space after another burn to place the active craft on a transfer orbit – but not until a certain number of orbits have passed. The parameters are:
  • Active: The current orbit
  • Passive: The current orbit of the target object
  • Transfer: The future orbit, which will produce the desired phasing
  • Active_Current_TA: The true anomaly of the active craft, in terms of its own orbit.
  • Passive_Current_TA: The true anomaly of the target object, in terms of its own orbit.
  • Desired_Seperation_At_Rendezvous: The desired distance, in meters, between the two craft at the completion of the rendezvous.
  • Num_Of_Orbits: The number of phasing orbits that should be completed before the conditions are correct to perform the rendezvous burn.
To work properly, the phasing for such a maneuver must be extremely accurate – more accurate than my perform burn logic can produce. However, the required level of accuracy can be produced by invoking this user defined instruction several times as follows:
  1. First, invoke it with the last parameter set to, say, 3.
  2. Perform the burn it returns.
  3. Invoke it again, this time with the last parameter (Num_Of_Orbits) set to 2.
  4. Perform the burn it returns.
  5. Invoke it yet again, this time with the last parameter set to 1.
  6. Perform the burn it returns.
  7. Finally, invoke the next user defined instruction to actually perform the rendezvous.
  8. Perform that burn.
Three phasing orbits will be sufficient to rendezvous with even a highly challenging target (T.T.) under unfavorable conditions (in a low inclination prograde orbit attempting to rendezvouses with a target in a retrograde orbit with high inclination). With less challenging targets (say, Luna rather than T.T.) in more favorable conditions (the orbital planes are close – within 10°) may only require 2 even 1 burn to setup proper phasing.

Also, be aware that the current code will happily increase apoapsis beyond the current sphere of influence in attempt to achieve rendezvous in the required number of orbits. It is the responsibility of the caller to identify that this has happened and take appropriate action (most likely, increase the number of phasing orbits requested).

This is the other phasing instruction that is highly likely to fail in some weird and mysterious case that I have not yet tested for. It seems to work right now...

While this function and the next can be used to rendezvous with a craft whose period is similar to the period of the current craft (for example, between a craft in low orbit and a space station also in low orbit) it produces burns that are very, very, very expensive and runs a high risk of large errors in position at the end of the rendezvous process. The problem is that the active craft’s orbit at the end of the rendezvous is all but certain to have a very different line of apse when compared to the target craft, and the only way to fix this is an enormous radial burn. Radial burns are only efficient if the orbit is very nearly circular which is also very unlikely be the case at the end of the rendezvous orbit, so vast amounts of delta-v will be spent correcting the line of apse. So… Do not do that.

This user defined instruction works quite well for transferring to a moon, however, and this is their primary purpose.



This calculates a maneuver to align the apse lines with the line of intersection between the two orbital planes while increase thing apoapsis so that the transfer orbit exactly touches the passive orbit at apoapsis. If the phasing has been properly set, the active craft will arrive at apoapsis at the same time the target object reaches the same point in space and rendezvous will occur. The parameters are:
  • Active: The current orbit
  • Passive: The current orbit of the target object
  • Transfer: The future orbit, after the phasing burn is completed
  • Active_Current_TA: The true anomaly of the active craft, in terms of its own orbit.
  • Passive_Current_TA: The true anomaly of the target object, in terms of its own orbit.
  • Desired_Seperation_At_Rendezvous: The desired distance, in meters, between the two craft at the completion of the rendezvous.
This instruction is blind – it calculates the transfer orbit assuming the phasing is correct. If it is not (and it almost never will be unless you have taken measures to ensure it is, see the previous instruction) then the rendezvous will fail.
Orbital Maneuvers #2 (2/2)


This calculates a maneuver to adjust the periapsis of a craft on a hyperbolic orbit. The parameters are:
  • Active: The current orbit
  • Transfer: The future orbit, after the burn has been completed
  • Active_Craft_TA: The true anomaly where the burn should occur. The caller should ensure that this is greater than the current true anomaly of the active craft.
  • New_Periapsis: The desired new periapsis value, in meters
There is no specific user defined instruction to adjust hyperbolic apoapsis. This is because it is not required – any of the existing functions that adjust apoapsis, including the very simple “Calculate Burn To Change AP,” will suffice for this purpose. As a corollary to the previous statement, any of the existing user instructions that change apoapsis can be used to enter a hyperbolic orbit – just specify a negative value for apoapsis and ensure that the semi-major axis is also negative (the formula for this is “( apoapsis + periapsis) / 2”, so you need to ensure that the negative apoapsis is larger than the positive periapsis).

However, changing the periapsis is not so easy -- the craft will never reach the apoapsis, so the existing logic will not work. This is implemented by:
  1. Calculating a hyperbolic orbit with the desired periapsis.
  2. Rotating the orbit until it intersects with the current orbit at "Active_Craft_TA."
There will be two possible rotations that satisfy the above conditions. One represents an orbit where the craft is approaching periapsis and one represents an orbit where the craft has already passed periapsis. The later orbit could be described as the retrograde version of the desired orbit.

I could not come up with a better way to determine which rotation is correct other than simply comparing the delta-v for both and choosing the one with the lower delta-v requirements.
Perform Burn (1/2)


This returns the amount of propellant required by an engine (or combination of several engines) whose ISP (a standard measure of engine efficiency) to produce the stated force. The parameters are:
  • inForce: The amount of force that will be produced, in Newtons.
  • inISP: The ISP of the engine(s) that will be used.
This function neither knows nor cares whether the actual engines attached to the craft can provide the specified level of thrust.


This function returns the amount of propellant required to produce a given amount of delta-v with an engine (or combination of engines) whose ISP is the stated value. The parameters are:
  • inDeltaV: The amount of delta-v to be produced, as a vector measured in meters / second.
  • inISP: The ISP of the engine(s) that will be used.
This is not a trivial formula – it considers the reduction in mass of the craft as propellant is burned. It also does not care if the required amount of fuel is actually available.


This function returns how long (in seconds) you will need to fire engine(s) to generate a specified amount of delta-v if the engine’s ISP is the specified value. The parameters are:
  • inForce: The combined force produced by all engines. This value is constant throughout the burn.
  • inISP: The ISP of the engine(s) being used.
  • inDeltaV: A vector, in meters per second, whose magnitude is the amount of delta-v to be gained.
Again, this formula considers the reduction in mass of the craft as propellant is burned off. And also again, this formula does not know, nor does it care, if sufficient propellant exists to complete the burn.


This function returns how long (in seconds) you will need to fire engine(s) to generate half the specified amount of delta-v if the engine’s ISP is the specified value. The parameters are:
  • inForce: The combined force produced by all engines. This value is constant throughout the burn.
  • inISP: The ISP of the engine(s) being used.
  • inDeltaV: A vector, in meters per second, whose magnitude is the amount of delta-v to be gained.
This not only considers the reduction in mass of the craft as propellant is burned off, it “knows” this is part of a larger burn and will return the time value that results in half the delta-v being gained before this time and half after this time. It is a messy equation, but people claim that it is correct…


Not meant to be called directly, this instruction determines several parameters associated with performing a burn. The parameters are:
  • in_Prefix_Str: The current orbit of the craft that will perform the burn.
  • inDeltaV: A PCI vector that represents the amount of delta-v to be generated
  • inTA: The true anomaly where the burn should occur, measured in terms of “in_Prefix_Str”
  • inNumOfOrbits: The number of additional orbits to wait before performing the burn.
The primary outputs are:

  • The starting (and maximum) value that will be used during this burn. This will normally be 1 (which corresponds with “100%”), but will be reduced if the magnitude of inDeltaV is small. To be exact, if the calculated length of the burn at full throttle is less than 10 seconds, the throttle will be reduced to the value that produces a burn whose duration is exactly 10 seconds.

  • This is how long, in seconds, the burn is expected to take.

  • This is the amount of time, in seconds, since the launch timestamp where the burn should start. This number considers the current orbit, the current position of the active craft, the specified true anomaly value of the burn, and any extra orbits that should be skipped prior to the burn. This value is set so that the craft would reach “inTA” after ½ the burn duration (as calculated from the previous function).

If “inTA” is greater than 2π (360°) then it will wait one or more orbits based on how many multiplies of 2π (360°) are present in “inTA.” This delay is in addition to any delay requested via “inNumOfOrbits.”

If the true anomaly of the current position of the active craft (as calculated from the Vizzy nav(position) block) is greater than "inTA," and the orbit is elliptical, then the burn will occur on the next orbit (after the craft passes periapsis). While this is obviously correct, this automatic adjustment is likely to cause problems if you are attempting to rendezvous two craft as it will invalidate the expected phasing. To avoid this problem, it is the responsibility of the caller to detect this and increment "inTA" by 2π .

If the true anomaly of the current position of the active craft is greater than "inTA" and the orbit is parabolic or hyperbolic, then... Well, it complains and starts the burn immediately. This will almost certainly not produce the desired results, however.


Not meant to be called directly, this instruction identifies one of three attributes as being “most important” for a requested burn. The parameters are:
  • in_Prefix_Str: The current orbit of the craft that will perform the burn
  • inDeltaV: A PCI vector that describes the burn to be performed
  • inTA: The true anomaly where the burn should occur
The three attributes that might be considered "most important" are:
  • If the largest component of the burn is in either the prograde or retrograde directions, then “a” (the semi-major axis) will be identified as the most important component.
  • If the largest component of the burn is in either the normal or anti-normal directions, then the angle between the PCI angular momentum (h) of the current orbit and the calculated target orbit will be identified as the most important component.
  • If the largest component of the burn is in either the radial-in or radial-out directions, then the angle between the PCI eccentricity vector (e) of the current orbit and the calculated target orbit will be identified as the most important component.

Two variables are returned:


  • Will contain either "a", "h" or "e" based on which attribute was identified as "most important."


  • Will contain a not-so-simple string that, when evaluated with the FUNK Vizzy block, will return the absolute value of the error between the current orbit and the target orbit in the key attribute.
This is only used in the "Complex" burn termination logic.
Perform Burn (2/2)


Not meant to be called directly, this instruction monitors the progress of a burn. It is meant to be called once per physics tick (or as often as possible, if once per tick is not possible) throughout the duration of the burn. It calculates and displays a range of parameters so that the player can monitor the progress of the burn and to control the current throttle setting and attitude of the active craft.

This block of code is used to implement two (out of three) of the burn logic options supported by this code:
  • “Moderate” (“Perform_Burn_Termination_Logic” = 2): In this mode, the engine’s throttle will be reduced as the burn approaches completion (configurable via a FUNK expression in “Init”), a PCI vector which is the difference between the amount of delta-v requested and the amount of delta-v generated will be used to control the attitude of the active craft, and the burn will end when the amount of delta-v to gain is less than 0.001 m/s.
  • “Complex” (“Perform_Burn_Termination_Logic” = 3): In this mode, the engine’s throttle will be reduced as the burn approaches completion (configurable via a FUNK expression in “Init”), the craft’s attitude will always be set to “inDelta_V” (the amount of delta-v requested), and the burn will end when the change in the key attribute measured between iterations changes sign – that is, it goes from being negative to positive or vice versa. This indicates that the key attribute has just passed its minimum value, and continuing the burn will only introduce additional error.



Not meant to be called directly, this instruction also monitors the progress of a burn. It is meant to be called once per physics tick throughout the duration of the burn.

This is only used when “Perform_Burn_Termination_Logic” = 1, requesting the “Trivial” burn termination logic be used. When activated, the engine remains at its starting throttle until the calculated duration has elapsed and then the throttle is set to zero. Amazingly, this very simple logic produces results at least as good, and generally better than the more complex logic used by the previous instruction.

In addition to the very trivial operative logic, this code also generates many, but not all, of the variables used in “Perform Burn Monitor.” These variables do not influence the progress of the burn, but are simply displayed to the user and in some cases recorded in the local log for later review.

The variable “Perform_Burn_PCI_Velocity_Accumulator” is used to control the burn in an indirect way. As long as this vector is (0, 0, 0), the burn will not be terminated due to no thrust being produced. This is necessary because the request to throttle up the engine and the first iteration of this loop are intended to occur during the same physics tick. Therefore, it is necessary to check to see how much thrust the engine is producing, and if that amount is greater than zero, set “Perform_Burn_PCI_Velocity_Accumulator” to a non-zero value. Currently, the display logic handles this, but if you removed this for some reason then you would need to add in this check.



This is the only user defined instruction in this block which is intended to be called from outside this block. It performs a burn at a specified true anomaly value. The parameters are:
  • in_Prefix_Str: The current orbit of the craft that will perform the burn.
  • inDeltaV: A PCI vector that represents the amount of delta-v to be generated
  • inTA: The true anomaly where the burn should occur, measured in terms of “in_Prefix_Str”
  • inNumOfOrbits: The number of additional orbits to wait before performing the burn.
This instruction:
  1. Calls “Calculate Burn Duration, Start Time, and Initial Throttle” to determine when the burn should start and how long it should continue.
  2. Loads “Perform_Burn_Orbit_Before_Time_Warp” with an orbit derived from the active craft’s current state vector (PCI position and velocity). This set of orbital parameters will incorporate any drift that might have been introduced due to use of the numerical integration.
  3. Loads “Perform_Burn_Planned_Orbit_Before_Time_Warp” with an orbit derived from the PCI position and velocity at “inTA” on the orbit calculated in the previous step, but with the “inDeltaV” added to the PCI velocity. This is the orbit that is expected upon completion of the burn.
  4. Uses time warp to wait until 20 seconds before the burn is supposed to start.
  5. Loads “Perform_Burn_Orbit_After_Time_Warp” with an orbit derived from the active craft’s state vector (PCI position and velocity). The parameters if this orbit should be the same as was calculated in step #2 – but will not be if the numerical integrator was running when this user defined instruction was called. It will be very, very close, however.
  6. Loads “Perform_Burn_Planned_Orbit_After_Time_Warp” with an orbit derived from a state vector as in step #5, but this time using “Perform_Burn_Orbit_After_Time_Warp” as the starting point (instead of “Perform_Burn_Planned_Orbit_Before_Time_Warp”).
  7. Points the craft in the direction given by “inDeltaV” until the calculated start time of the burn arrives.
  8. Sets the throttle to the calculated start throttle (normally 100%) and starts a loop that invokes the proper monitor (depending on “Perform_Burn_Termination_Logic”). The loop exits when the amount of thrust produced is zero and the amount of velocity gained is greater than 0.
  9. Outputs a range of data to the local log to allow the user to evaluate the accuracy of the burn.

There are three methods that can be used to determine how the burn will be terminated. They are:
  • 1 = Trivial: The burn duration and start time are calculated, then it waits for the start time, starts the "clock," throttles up to 100%, waits until the expected duration has completed, sets the throttle to 0%, and finally waits until no thrust is produced.
  • 2 = Moderate: This starts with calculating the burn duration and start time then waiting for the start time and setting the throttle to 100%, but then it determines how much delta-v has been generated as the burn proceeds, points the craft in the direction required to most efficiently gaining the remaining delta-v, throttles down smoothly as the amount of delta-v remaining drops below 10 m/s, setting the throttle to 0% when the delta-v to gain drops to 0.01 or less. The burn officially ends when the thrust produced drops to zero.
  • 3 = Complex: This converts the PCI delta-v requested into Prograde / Normal / Radial components and determines which of those components is larger. It then identifies one of the three orbital elements at the "most important" (semi-major axis for prograde, inclination for normal, or eccentricity vector for radial). After this calculation, the burn proceeds mostly as described in the tribal case, but the burn only terminates when the change in the "most attribute" starts moving in the opposite direction.
Which option is used can be set via the "Init" method (via the global variable "Perform_Burn_Burn_Termination_Logic").

Which is best? My other guide covers this in great depth in the "Which method of performing burns is best?" section, but the conclusion is... The Trivial (1) burn logic is best.
Compound Orbital Maneuvers (1/2)
These are “pre-packaged” groups of the simple maneuvers to perform specific tasks. Unlike the simple maneuvers described earlier, these actually execute the maneuvers as they go, so a single invocation suffices to perform the requested maneuver.



This user defined instruction will execute a series of burns with a goal of having the active craft rendezvous with the current target with a specified separation at the rendezvous point. The parameters are:
  • Desired_Sep_At_End: This is the desired separation (in meters) between the active craft and the targeted object at the end of the rendezvous. Given the errors introduced by the limitations of the “Perform Burn” logic, this should almost always be zero.
  • Zero_Rel_Vel_At_End: If set to “true,” a burn will occur at the anticipated rendezvous point to match the target’s orbital velocity.
It invokes the following simple orbital maneuvers in sequence:
  • “Calculate Burn To Match Planes”
  • “Calculate Burn To Change PE” To change the periapsis of the active craft to match the target object, less the value in “Desired_Sep_At_End.”
  • “Calculate Burn To Adjust Phasing At PE” both to rotate the apse line of the active craft to align with the target object and to increase apoapsis to whatever value is required to ensure both craft arrive at periapsis at the same time after two orbits of the active craft.
  • Invokes "Calculate Burn To Adjust Phasing At PE" a second time, this time looking to align the aspe lines and adjust apoapsis to arrive at periapsis after one orbit of the active craft. This should produce a very small burn.
  • If “Zero_Rel_Vel_At_End” is “true,” then it performs a burn to set the active craft’s apoapsis to the target craft’s apoapsis at periapsis. Assuming everything has worked properly, the only difference between the active craft’s orbit and the target craft’s orbit will be the apoapsis value, so this will zero out relative velocity.
    As will be true throughout, the last step does not necessarily zero out the relative velocity at the closest point. It would if the burns were 100% accurate, but… They are not, and since the code assumes perfectly accurate burns then there will be some relative velocity (generally, 1-5 m/s) remain at the end of the burn.
This is reasonably accurate, producing separations of ~100 km with one phasing orbit but -5 km with two phasing orbits with orbiting craft in mostly similar orbits. While it can be used to transfer to a moon, the requirement to perform a plane change maneuver may make it more expensive than other options.

There are two advantages to this maneuver versus the other maneuvers described later:
  • The total delta-v of this this maneuver will always be less expensive than "Execute Fast Rendezvous" and will almost certainly be less than "Execute Direct Transfer To Target."
  • The relative velocities of the craft at rendezvous will be minimal and (assuming all the burns were performed accurately) will be limited to the prograde / retrograde direction. Thus, a burn to cancel out residual velocity will be highly efficient.

This disadvantage is that it requires more burns, which means that the rendezvous will take up more real-time as time compression has to be turned down each time a burn is performed.


This user defined instruction will execute a series of burns with a goal of having the active craft rendezvous with the current target with a specified separation at the rendezvous point. The parameters are:
  • Desired_Sep_At_End: This is the desired separation (in meters) between the active craft and the targeted object at the end of the rendezvous. Given the errors introduced by the limitations of the “Perform Burn” logic, this should almost always be zero.
  • Zero_Rel_Vel_At_End: If set to “true,” a burn will occur at the anticipated rendezvous point to match the target’s orbital velocity.
It invokes the following simple orbital maneuvers in sequence:
  1. “Calculate Burn To Match Planes”
  2. “Calculate Burn to Change PE” To change the periapsis to exactly match the target object
  3. “Calculate Burn To Setup Rendezvous With Co-Planar Target”
  4. If requested, it looks at the calculated velocity of the target at the rendezvous true anomaly (relative to the target object’s orbit) and the calculated velocity of the active craft at the rendezvous true anomaly (relative to the active craft’s orbit) and performs a burn to match the velocities.

This is highly accurate and works well as long as the target’s orbit does not overlap with the active craft’s orbit before “Calculate Burn To Setup Rendezvous With Co-Planar Target.” If overlap does exist, there is logic to detect that problems have occurred and stall the transfer until the conditions are more favorable.

This also works well with transferring to moons – but be careful not to zero out relative velocity in this case. 😊
Compound Orbital Maneuvers (2/3)


This user defined instruction will execute a series of burns with a goal of having the active craft transfer to a moon without first aligning planes. It can take quite a long time (days or weeks of game time) to complete if the active craft and moon have unfavorable initial positions. The parameters are:
  • “Moon_Name_Str”: The name of the moon to be transfer to, as a string. This must, of course, be a moon that is orbiting the current central body.
  • “Max_AP_During_Transfer”: When phasing burns are setup, it will not create a phasing orbit with an apoapsis value greater than this. Instead, it will add additional phasing orbits. This allows you to ensure that the phasing orbits do not cross over with the orbits of other moons, preventing unwanted flybys.
    If this is set to less than the apoapsis of the current orbit, this will be set to 110% of the apoapsis of the current orbit. This will require lots and lots of phasing orbits, but…
  • “Min_Num_Of_Phasing_Orbits”: This sets the minimum number of phasing orbits that will be performed. Due to errors introduced by the limitations of the “Perform Burn” logic it is strongly recommended that this be set to at least two, as it will attempt to refine the phasing orbit at each opportunity.
    If this is set to less than 1, then this is set to 1.
  • “Desired_Seperation_After_Transfer”: The desired separation after transfer ignoring the gravity of the destination moon. Even if perfectly accurate burns were possible, the actual separation at the moon will be significantly less than this value, with how much less varying in a non-linear way as this value drops towards zero. The purpose of this parameter is to reduce the magnitude of the burn that the next parameter will setup.
  • “PE_At_Moon”: After the active craft enters the sphere of influence of the moon, this parameter is used to adjust the periapsis of the hyperbolic fly-by trajectory. This will be a pure radial-in / radial-out burn (and therefore rather inefficient), but it does ensure that the periapsis is the correct value. Adjusting the previous parameter will reduce the magnitude of this burn.
  • “Circulize”: If set to “true,” then a burn will be performed at the adjusted hyperbolic periapsis to enter a circular orbit of the moon.
It invokes the following simple orbital maneuvers in sequence:
  1. “Init Burn To Setup Rendezvous Along PCI Apse Line”
  2. “Calculate Burn To Setup Phasing For Rendezvous With Non-CoPlanar Target” repeatedly until the apoapsis of the calculated phasing orbit is less than “Max_AP_During_Transfer,” increasing the number of phasing orbits by one each time.
  3. It then calls “Calculate Burn To Setup Phasing For Rendezvous With Non-CoPlanar Target” repeatedly again, this time starting with the number of phasing orbits calculated in the previous step, performing the required burn, and repeating (with the number of phasing orbits reduced by one). If the burn is judged to be too small (0.05 m/s) then the burn is skipped altogether.
  4. “Calculate Burn To Setup Rendezvous Along PCI Apse Line,” which actually performs the rendezvous.
  5. “Calculate All Patched Conics” to identify the expected sphere of influence change.
  6. “Monitor Several SOI Changes” is used to wait until the next sphere of influence change.
  7. “Calculate Burn To Adjust Hyperbolic Periapsis” to perform a radial-in / out burn to adjust the hyperbolic periapsis at the moon
  8. Finally, if “Circulize” is set to “true,” it calls “Calculate Burn To Change AP” to set the apoapsis equal to the current hyperbolic periapsis – 10 meters.
This works very well if the starting orbit is fairly low and fairly circular.

When the orbit is highly elliptical, it is likely that the orbit will intersect with one or more of the other moons of the central body during the phasing process, which this code does not detect and therefore does not attempt to address. An unplanned flyby is almost certain to wreck the phasing far beyond its ability to compensate for, which will prevent the desired transfer from occurring. This can be avoided by setting “Max_AP_During_Transfer” to the periapsis of the moon with the lowest orbit plus making sure that apoapsis of the initial orbit is less than this value.

It may work when the initial orbit is circular and totally contains the target, but this is not tested.

The advantage of this instruction is that it eliminates the need for a separate burn to adjust planes. Now, to be clear, a burn does occur to match planes but it occurs as part of the "Circulize the orbit at the hyperbolic periapsis" burn. By matching planes at this point there are two enormous advantages that reduce delta-v cost:
  1. The proximity of the craft to the moon (which is in the correct plane) dramatically reduces the delta-v required. Even no burn was performed at all the simple act of performing the fly by would adjust the craft's inclination towards the inclination of the moon. This is due to the gravity of the moon, of course.
  2. The altitude at rendezvous is likely much higher than the altitude at apoapsis before the rendezvous process was started. Since the whole point of this burn is to ensure that the rendezvous occurs at a point along the line of intersection between the active craft's orbital plane and the target's orbital plane this will reduce the delta-v requirements tremendously.

For these reasons this is, by far, the best maneuver to transfer to a moon if the planes are not already aligned. If the planes are aligned, then "Fast Rendezvous" will be, well, faster. 😊
Compound Orbital Maneuvers (3/3)


This user defined instructions will execute a series of burns with a goal of rendezvous the active craft and the targeted object without first aligning planes. How long it takes is highly variable and it will spend a very large amount of delta-v (twice as much or more) if the orbits of the active craft and target craft are highly similar. Not really recommended, but included for the sake of completeness. 😊The parameters are:
  • “Max_AP_During_Transfer”: When phasing burns are setup, it will not create a phasing orbit with an apoapsis value greater than this. Instead, it will add additional phasing orbits. This allows you to ensure that the phasing orbits do not cross over with the orbits of other moons, preventing unwanted flybys.
    If this is set to less than the apoapsis of the current orbit, this will be set to 110% of the apoapsis of the current orbit. This will require lots and lots of phasing orbits, but…
  • “Min_Num_Of_Phasing_Orbits”: This sets the minimum number of phasing orbits that will be performed. Due to errors introduced by the limitations of the “Perform Burn” logic it is strongly recommended that this be set to at least two, as it will attempt to refine the phasing orbit at each opportunity.
    If this is set to less than 1, then this is reset to 1.
  • “Desired_Seperation_After_Transfer”: The desired separation at rendezvous.
  • “Zero_Rel_Vel”: If “true,” then one last burn will be performed to match calculated velocities at the calculated rendezvous point.
If the transfer (not original) orbit has an e value much greater than 0 (say, 0.1 or more) then the burn to zero relative velocities at rendezvous will require an enormous (and very inefficient) radial component to align the apse lines. Having such an e value is almost certain to occur.

This is caused by two factors acting in combination:
  • In order to ensure the rendezvous occurs along the line of intersection between the two orbital planes, the apse line of the active craft is rotated to match the line of intersection between the active craft’s orbital plane and the target’s orbital plane. This is relatively efficient, because the apoapsis of the active craft’s orbit is changed at the same time (to setup phasing for the future rendezvous) so the bulk of the burn will be in the prograde direction. 99% of the apse rotation is handled by adjusting the burn’s true anomaly to occur slightly earlier or slightly later than the desired new periapsis point, and the remaining 1% is handled by adding a small radial component to the burn.
    The above is not some sort of “special case” logic incorporated into the code. Rather, it is a natural consequence of how the math associated with orbital mechanics works.
    If the apoapsis value generated by the phasing logic is close to the apoapsis of the active craft’s initial orbit, the above will be less true. This will occur when a very large number of phasing orbits is required (due to setting the “Max_AP_During_Transfer” to a low value or setting “Min_Num_of_Phasing_Orbits” to a high value) and / or the orbits are very close to being properly aligned already. In such a case, a large radial component will be produced to achieve the desired rotation while holding apoapsis mostly constant.
    As a result of this rotation the odds of that the apse line of active craft's orbit will align with the apse line of the target orbit is nearly zero.
  • To zero out relative velocity between two craft in proximity is equivalent to saying “Perform a burn to match all orbital parameters between the active craft and the target.” This is, in fact, exactly what this code does throughout to switch from one orbit to another and that is exactly how this maneuver is implemented in Vizzy. However… Part of matching all orbital parameters is to align the apse lines (or “align the direction of the e vector”), and this is a very expensive maneuver unless coupled with a large change in either apoapsis or periapsis (but not both at the same time). In the case of a rendezvous, there will almost always be a large change in both apoapsis and periapsis that needs to be performed and a large rotation will be required to align the apse planes. Thus, the zero out relative velocities burn will necessarily have a very large radial component, and that radial burns are very inefficient unless e is close to zero.
As a result of this, using this sort of rendezvous between two craft in roughly similar orbits is not recommended. The code is included more for the sake of completeness rather than as a useful tool to be used for rendezvous between two craft.

This is not an issue with moons because there is no need to align apse lines upon arrival at the moon. Once the active craft enters the sphere of influence of the moon the apse line of the previous orbital path no longer exists and the circularization burn will accept whatever apse line the hyperbolic fly-by trajectory happens to have.
If you the target craft is in a very distant orbit (outside the orbits of all the moons, say at 90% of the sphere of influence), this logic will produce much better results. The low orbital velocity at rendezvous will dramatically reduce the cost of rotating the apse lines and the enormous change required in periapsis (assuming you the initial orbit is a mostly circular low orbit) will combine to result in a reasonably costed zero relative velocity burn. This is, of course, a very unusual place to put a space station, but it could be done…
If you can time your launch such that the periapsis or apoapsis of the target craft's orbit lies on the line of intersection between the orbital planes (aka "at the ascending or descending node") then the above concerns disappear and this maneuver will work fine.

Of course, doing this would be far, far more difficult than simply launching into the correct orbital plane and using "Fast Rendezvous" and far more difficult than simply launching directly to the rendezvous. But it could be done, in theory.
Patched Conics #1
Starting from the top:



This user defined instruction is not expected to be called from user code. It determines if the craft’s current orbit intersects with any of the moons (or planets, if the current sphere of influence is the sun) that orbit around the current central body. If so, it determines where the intersection occurs and generates a state vector suitable for calculating the new orbit in the new sphere of influence. The parameters are:
  • “Orbit”: The current orbit of the active craft
  • “Current_TA”: The true anomaly of the active craft relative to its orbit.
  • “Time_From_Launch”: The number of seconds that the craft has existed when the craft is at the “Current_TA” value. This is called “Time From Launch” in Vizzy, thus the name.
  • “Expected_Planet_Name”: If not an empty string, the code only checks for intersections with moons with the specified name.
    Setting this to anything other than an empty string may produce inaccurate results.
The key return variables are:

  • This is the expected time from launch and true anomaly of the active craft when the sphere of influence change occurs.

  • These two variables are a valid state vector in the new sphere of influence and can be used to calculate the new orbit. Additionally, once the new orbit is calculated “SOI_Change_Active_PCI_Position_At_SOI_Change_New_SOI” can be used to calculate the true anomaly of the craft immediately after the sphere of influence change.
This takes quite a long time (0.5 seconds) to execute when the active craft’s initial orbit path crosses the orbital paths of several moons. If an actual SOI change is detected it takes even longer (a total of 1 – 2 seconds). This is not something that you can call while monitoring the performance of a burn, for example.



This user defined instruction is not expected to be called from user code. It determines if the craft’s current orbit exits the current sphere of influence into the parent’s sphere of influence. If it does, it determines where the departure occurs and generates a state vector that can be used to calculate the new orbit in the new sphere of influence. The parameters are:
  • “Orbit”: The current orbit of the active craft
  • “Current_TA”: The true anomaly of the active craft relative to its orbit.
  • “Time_From_Launch”: The number of seconds that the craft has existed when the craft is at the “Current_TA” value. This is called “Time From Launch” in Vizzy, thus the name.
The key return variables are:

  • This is the expected time from launch and true anomaly of the active craft when the sphere of influence change occurs.

  • These two variables are a valid state vector in the new sphere of influence and can be used to calculate the new orbit. Additionally, once the new orbit is calculated “SOI_Change_Active_PCI_Position_At_SOI_Change_New_SOI” can be used to calculate the true anomaly of the craft immediately after the sphere of influence change.
This is quite fast – detecting whether the active craft exits the current SOI is simply a matter of checking to see if the apoapsis is greater than the SOI radius (for elliptical orbits) or if the orbit is not elliptical. Be aware, however, that calling this without first calling “Find Next Entrance Into Child SOI” may produce incorrect results – this logic will not detect if the craft’s orbit intersects with a child SOI before it exits the current SOI.



This handles automatically updating the orbit “Current” to reflect the new orbit after an SOI change.

While this technically works, the asynchronous nature of the update makes it highly problematic. Thus, a global variable "Global_Auto_Update_Current_Orbit_After_SOI_Change" is provided with a default value of "false" (set in "Init"). With this setting, it is the responsibility of the main thread to identify that an SOI change has occured and recalculate the orbit. The current code uses "Monitor_Several_SOI_Changes" to perform this task.
Patched Conics #2


This user defined instruction is not expected to be called from user code. It checks to see if the craft exits the current SOI to the parent’s SOI then checks to see if the craft enters into a child SOI. If both occur, it determines which occurs first and returns only that value. Finally, it calculates the next orbital patch if applicable. The parameters are:
  • “Current_Orbit”: The current orbit of the active craft
  • “Patched_Conic_Orbit”: The name of the orbit that will be loaded with the parameters of the next orbital patch, if it exists.
  • “Current_TA”: The true anomaly of the active craft relative to its orbit.
  • “Time_From_Launch”: The number of seconds that the craft has existed when the craft is at the “Current_TA” value.
The primary return value is the orbital parameters of the next patch which are placed in "Patched_Conic_Orbit." However, if no new orbital patch is found then "Patched_Conic_Orbit" will be left alone, and will either contain stale values (if you are re-using the string) or cause Vizzy to crash (if you have not previously use that particular string). Therefore, you can check:
To determine if a new orbital patch was created. This will be set to the string "NaN" if no new orbital patch was created, or to the true anomaly (in terms of "Current_Orbit") where the sphere of influence change is expected to occur.

As this calls “Find Next Entrance Into Child SOI,” this is also very slow – 1-2 seconds if a SOI change occurs, 0.5 seconds if no sphere of influence change.


This user defined instruction checks for and calculates all patched conics that apply to the current orbit. The parameters are:
  • “Current_Orbit”: The current orbit of the active craft
  • “Current_TA”: The true anomaly of the active craft relative to its orbit
  • “Time_From_Launch”: The number of seconds the craft has existed when the craft is at the “Current_TA” value
The patched conics orbital parameter name strings will be generated automatically be appending a number to the name of the current orbit. For example, if “Current_Orbit” = “Current”, the first patched conic will be named “Current_1”, the second “Current_2”, and so forth. The number of patched conics created is returned in:

There are a number of important caveats to this instruction:
  1. It is very slow. Each patch that needs to be calculated will take 2-5 seconds, and in theory there could be an infinite number of patches to calculate. This should only be called when you expect an SOI change to occur.
  2. No attempt is made to clear out "stale" orbital patches. If you call this and it creates 3 orbital patches, and later call it and it only creates 1 patch, there will still be 3 patches available ("Current", "Current_1", "Current_2", "Current_3") but only 1 will be valid. "Patched_Conic_Cnt" will tell you what the last valid patch is, of course.
  3. The flip side of the previous point is that if you try to access orbital parameters for a string that has not yet been initialized then the current thread in Vizzy will crash. Again, if you do not try to access orbital patches beyond "Patched_Conic_Cnt" then you will be fine.
  4. Finally, when an sphere of influence change there is no code to "move up" the orbital patches. So you can end up in a situation where you have "Current" and "Current_1" representing more or less the same orbit.
Orbit & Text formatting
This section contains code that is used to automate the formatting of various values. It is primarily intended for troubleshooting purposes although it is also used for display purposes.



This user defined function returns a string that contains a nicely formatted vector with specified amounts of precision. The parameters are:
  • “inVector”: The vector to be formatted
  • “inPercision”: The number of digits to the right of the decimal point to display for all numbers returned.
If the magnitude of the vector falls outside of the range 0.009 – 1.001, then the formatted vector (with “inPercision” set to 4) will look like this:

476.5344 (-2.7046, 474.7382, -41.2932)

Otherwise, the magnitude of the vector will be omitted, and it will look like this:

(-0.9864, 0.0338, -0.1610)

The reason the magnitude of the vector is omitted when the vector length is close to 1 is to make it easier to combine displaying a vectors magnitude combined with a normalized version of the components. It is much easier to compare the direction of two vectors when both vectors are normalized, and I needed to do that a lot while troubleshooting.

This user defined function returns a string that contains a nicely formatted altitude. The parameters are:
  • “inDistFromCenterOfPlanet”: The distance from the center of the planet, in meters.
  • “Planet_Name”: A string that is the name of the planet the previous parameter is relative to.
All this does is subtract “inDistFromCenterOfPlanet” from the radius of the planet whose name is in “Planet_Name,” and pass the result to the Vizzy “Friendly [Distance]” block.

The primary use of this is to make direct comparisons of apoapsis and periapsis values with the values returned in the game UI.
The Vizzy “Friendly [Distance]” block does not handle negative numbers correctly. It will format a distance of -5000 meters as “5 km,” dropping the sign. This creates some oddities when performing direct comparisons between apoapsis and periapsis values, but is acceptable in most cases.


This user defined function returns a string that contains a nicely formatted set of orbital parameters. The parameter is:
  • “in_Prefix_Str”: The name of the orbit to be displayed.
All floating-point numbers will be displayed with 4 digits of precision.

This looks like this:

Note that the numbers in parenthesis for AP and PE have been passed through “Format Altitude Friendly,” and thus do not correspond directly to the raw AP / PE numbers displayed (the radius of the planet has been subtracted). Also, the values shown in parenthesis for “e” and “h” have been normalized for easy comparison between two different orbits.


This user defined instruction creates a string that contains a nicely formatted comparison between two sets of orbital parameters. The parameters are:
  • “Orbit_1”: The first orbit to be displayed
  • “Orbit_2”: The second orbit to be displayed
This is a user defined instruction rather than a user defined function simply to limit the length of the “format” Vizzy blocks to only insanely long instead of “you gotta be kidding me” long. The formatted string will be stored in the variable “Compare_Orbits_Output_String,” and looks like this:

There are several caveats that apply to this:
  • First, it makes extensive use of the <pos=xx%> tag to position the columns. The values that I have chosen work well on my screen – but the odds of it looking correct on a mobile device are nearly zero. Nothing I can do about this, I am afraid – there is no way to detect the width of the screen from Vizzy. This may result in text overlapping into a garbled and unreadable mess.
  • Second, the true anomaly value (used to populate the “TA” row, and the Pos and Vel rows) is based on the current position of the active craft. This works well for its intended purpose (which is displaying the difference between the expected results of a burn and the actual results of a burn, but would produce incorrect results in other contexts. True anomaly really should be a parameter rather than calculated from the current position.
  • Finally, the “%” columns is… Suspect. The problem is that for lots of the rows it is not clear what value to use in the denominator to produce the percentage. For apoapsis, for example, it displays the difference between the first apoapsis and the second apoapsis values, divided by the first apoapsis value. Should the denominator be the half the sum of the first apoapsis and second apoapsis? I can make a valid argument either way, but I had to pick out one way to implement.
This is only used to compare the expected and actual results of burns.
Orbit & SOI Monitors
These are intended to periodically log calculated and actual position and velocity vectors to the local log, with a goal of detecting errors in the calculated values. Potentially, these could be used for display purposes while the craft is idle or under manual control, but the current code does not do this.


This user defined instruction generates a nicely formatted string that compares calculated and actual values for PCI / Perifocal Position / Velocity. The primary purpose is to identify deltas between these two values for further troubleshooting. To be useful, this needs to be invoked from a loop. The parameters are:
  • “Active_Craft_Orbit”: The orbit that the active craft is currently in.
  • “Monitor_Start_Launch_Timestamp”: The timestamp when this monitor session started. This is used to determine how much time has elapsed since the initial invocation to periodically write information to the local log.
  • “Reporting_Interval”: In seconds, how long should pass between writes to the local log.
  • “Log_Only”: If set to “false,” the generated string is passed to the Vizzy “display” block when it is written to the local log.
The primary output variable is:

Which always contains the nicely formatted string that may have been written to the log.

When I use this instruction I always set the “Log_Only” parameter to “true,” then manually display “Monitor_Output_String” once per physics tick.

This output looks like this:



This user defined instruction works mostly the same as the previous, but is specifically intended to monitor SOI changes. It therefore omits some of the parameters that “Monitor Orbit” reports on (specifically, the perifocal versions of position and velocity) while adding parameters that are more relevant to a sphere of influence change (the PCI position of the next body and the distance to the SOI boundary). The parameters are:
  • “Active_Craft_Orbit”: The orbit that the active craft is currently in.
  • “New_Planet_Orbit”: The orbit of the planet whose SOI is being entered (if transitioning to a child SOI) or exited (if exiting to the parent SOI)
  • “New_Planet_Name”: A string with the name of the next SOI
  • “Expected_SOI_Change_Launch_Timestamp”: The time, from the time that the ship was created, when the SOI change is predicted to occur
  • “Expected_SOI_Change_TA_Active_Craft”: The true anomaly value of the active craft (measured relative to its own orbit) where the SOI change is predicted to occur
  • “Expected_SOI_Change_TA_Planet”: The true anomaly value of the moon (measured relative to its own orbit) where the SOI change is predicted to occur
  • “Monitor_Start_Launch_Timestamp”: The time, from the time the ship was created, at which the monitor was first invoked in the current session
  • “Reporting_Interval”: How frequently messages should be written to the log. If this falls between 0 and 1 and the orbital is elliptical, this is treated as a “fraction of a period” – 0.25 will result in 4 messages being written to a log per orbit. If it is greater than 1, it is viewed as “number of seconds to wait between log messages.”
  • “Log_Only”: If “true,” messages are only written to the local log. Otherwise, messages are also displayed (via the Vizzy “Display []”) for the user to see.
The primary output variable is:

Which always contains the nicely formatted string that may have been written to the log.

When I use this instruction, I always set the “Log_Only” parameter to “true,” then manually display “Monitor_Output_String” once per physics tick.

This is an example of the local log output:


This user instruction will monitor multiple SOI changes in one go. Perhaps more usefully, it includes a while loop, so it can be invoked once and will only return after the specified number of SOI changes has occurred. It does, however, require that “Calculate All Patched Conics” be run before invoking this instruction, as it depends on the extended attributes that “Calculate All Patched Conics” creates. The parameters are:
  • “Orbit”: The orbit that the active craft is currently in.
  • “Num_Of_SOI_Changes_To_Monitor”: This is the number of times the SOI should change before the instruction exits.
  • “Time_To_Stop_Warp_Short_Of_SOI_Change”: In seconds, how long time warp should stop before the SOI change. This should be set to a low value to maximize accuracy (0 seconds should work).
  • “Time_Freq_To_Write_Status_To_Log”: This is passed to “Monitor SOI Change” as “Reporting_Interval” and controls how frequently snapshots are written to the local log.
The most common use of this is not troubleshooting – instead, it is highly useful to use this instruction to time warp to the next SOI change.
If there is an unexpected SOI change, an expected SOI change does not occur, or the number of SOI changes specified in “Num_Of_SOI_Changes_To_Monitor” does not occur this user instruction will never return. As long as you call “Calculated All Patched Conics” immediately before you invoke this you should be OK.
Conclusion
Well, there you go -- everything that you need to get started with automating orbital maneuvers. If you find this useful, please leave a comment here, especially if you use this as a base to create a user interface.

An interesting takeaway from all of this is that the hardest part (by far) is not the math -- yes, it is complex, but relatively straightforward once you have invested the time to understand it. The hard part is to use that math to create orbital maneuvers, especially when setting up phasing. Multiple cases need to be considered -- some of the questions I wrestled with include:
  1. What if the time to rendezvous is positive? Negative?
  2. How many orbits should the passive craft make before it reaches the rendezvous?
  3. How many orbits should the active craft make before the rendezvous?
  4. What should be done to avoid unwanted sphere of influence changes during phasing?
  5. How do we deal with errors from inaccurate burns?
  6. Do we need to worry about unwanted collisions at the end of the rendezvous? If so, how to do that?
And these are just some of the questions that I've run into recently trying to get phasing to work properly. Its much harder than the simple orbital mechanics would make it out to be. And this is true (although to lesser degree) with the other simple orbital maneuvers instructions. Its easy to say (as I did above) just "Calculate the desired final orbit, find the points of intersection between the final orbit and the current orbit, calculate the velocity of the craft in both orbits, subtract the velocity from the current orbit from the previous orbit," but the actual implementation is rarely this easy.

Making matters harder still is the enormous amount of time that testing requires -- one testing cycle takes 5-10 minutes (even with time compression). I wrote this entire section, plus did some minor cleanup elsewhere in this guide, while Juno was busily churning through an automated rendezvous.

So... Yeah, while you don't need much in the way of math to implement orbital maneuvers, it does require a great deal of patience to get it to work.