A Peek Into Stellaris 1.80

Some months ago, our local astronomy club received an email with a rather unusual enquiry. The person wanted to know how often the following astronomical phenomena occur simultaneously:

  • Spring equinox on a Wednesday between sunrise and sunset
  • A full moon occurs between sunrise on that day and sunset the evening before

While I could only wonder why anyone would want to know this, the type of enquiry was interesting, because my program Stellaris features an Event Finder tool designed specifically to find the dates of various events, including ones whose definition is rather arbitrary. This flexibility is achieved by using the StellarScript language to define the events.

However, this concrete question was beyond the capabilities of StellarScript at the time. The good thing about that was that it demonstrated very well what StellarScript was missing. I was busy working on Stellaris 1.70 at the time, with a release planned in the near future, and while the StellarScript enhancements required to answer the question couldn't make it into that release, they were the first feature to be added when work on Stellaris 1.80 commenced.

Which StellarScript features were already in place, and what was missing?

As a reminder, the Event Finder searches for events by repeatedly evaluating a boolean StellarScript expression for time instants in a time range. One defines the expression such that it evaluates to "true" when the event occurs. The date and time where the search begins and ends are defined by the user, as is the time interval from one checked instant to the next. The interval is typically one day or one hour.

Spring Equinox

Finding the date and time of spring equinox was easy - this is achieved by checking when the Sun's equatorial longitude reaches 0°. The StellarScript expression that calculates the Sun's equatorial longitude is Sun.Lon(Equ). Naturally, we cannot simply compare this value to 0, as in

Sun.Lon(Equ) == 0

because whatever time instant we choose for the calculation, the result will not happen to be exactly 0. StellarScript provides operators <<= (meaning the calculated value becomes less or remains equal from one evaluation to the next) and >>= (the value becomes greater or remains equal) for such purposes.

Since the Sun's equatorial longitude increases from 0° to 360°, we basically want to write Sun.Lon(Equ) >>= 0. There is a small trap here, though: Lon returns the longitude in the range [0..360[. At 360, that value wraps around to 0, so it is never negative, and so can never change from < 0 to >= 0. We could try to cheat by writing something like Sun.Lon(Equ) <<= 0.1, but this is not a good approach. The value 0.1 is too arbitrary, and we are relying on the time interval being sufficiently small (if the longitude is 359.99° at one instant and 0.11° at the next, the search misses the event). A better approach is AngleSum(Sun.Lon(Equ), 180) >>= 180, which is the same from a mathematical point of view, but more reliable for our purposes. The AngleSum function ensures the left side of the comparison remains in the range [0..360[. The value of 180 was chosen because its distance from 0° is the greatest, minimizing the danger of missing an event.

So finding the time of spring equinox was already possible in Stellaris 1.70 and is achieved with the StellarScript expression

AngleSum(Sun.Lon(Equ), 180) >>= 180

What we have so far gives us all time instants where spring equinox occurs. Note that this doesn’t yet answer the actual question, which was “when does spring equinox occur between runrise and sunset?”. We will return to this problem below.

The Day of the Week

The next step towards our goal is to identify those spring equinoxes that occur on a Wednesday. This in itself is very easy, but nonetheless required an extension to the StellarScript syntax. There is now a WeekDay() function that returns the day of the week, and the returned value can be compared against symbols "Monday", "Tuesday", etc. (All extension mentioned in this article will be available in Stellaris 1.80, scheduled for release in late 2014.)

The StellarScript expression thus becomes

AngleDiff(Sun.Lon(Equ), 180) >>= 180 And
WeekDay() == Wednesday

And we have the time instant of all spring equinoxes that occur on a Wednesday.

Full Moon (1)

The next step in our task is especially interesting, because it required a conceptual extension of what is possible with StellarScript. We need to check whether a full moon occurs between sunrise on the same day and sunset the day before. Recall that events are searched for by evaluating the StellarScript expression for successive time instants, defined by a constant time interval specified in advance. We shall use the term current instant to refer to the time instant for which the expression is currently being evaluated./p>

The Moon’s age can be readily calculated by calling Moon.Age() (an explanation why Age is called instead of Phase is given below). It is important to note that this determines the age for the current instant. What we need here, though, is a method to calculate the age for arbitrary instants - in this case, the times of sunset on the day the search is currently at, and the time of sunrise the day before.


This observation let to the concept of contexts in StellarScript. A context is simply the entirety of information, required for evaluating a function, that is not passed directly by the caller, but assumed to be available via some external mechanism. Put simply, evaluating a function call like Sun.Lon(Equ) or WeekDay() requires that a time instant is somehow specified - the call makes no sense otherwise. Because a calculation is usually performed for the current instant, the instant is not specified by the StellarScript code.

With this notion of context, we can extend StellarScript to allow the definition of a new context while searching. A context is defined by a date and the time of day (the observer's location is a natural candidate for future enhancements, since it also affects calculations). Functions can then be calculated for the new context, the point being that the context is created dynamically, i.e. during the search, from data that is not available before the search is started.

A context's date is defined by one of the symbols PreviousDay, CurrentDay, or NextDay. A context's time of day is defined by a numeric expression whose return value is interpreted as the time of day.

A concrete example demonstrates the syntax:

{Moon.Age() At Sun.Rise(Lct) On CurrentDay}

This calculates the age of the Moon at the time of sunrise on the day defined by the current instant, whereas


simply calculates the age of the Moon at the current instant.

Points to note:

  • Curly braces indicate the definition of a new context.
  • A new context can use the “At” keyword to define a new time, and it can use the “On” keyword to define a new date.
  • All calculations inside the curly braces are performed in the new context. In the example above, Moon.Age() is calculated for the new context as opposed to the previously active context.

Full Moon (2)

Returning to our initial goal, how can we apply the context mechanism to check whether a full moon occurs between sunrise on the current day and sunset the day before?

We already know how to calculate the age of the Moon at the two instants in question. At sunrise on the current day, it is

{Moon.Age() At Sun.Rise(Lct)}

(note that the date defaults to the current date, so “On CurrentDay” can be omitted) and at sunset the day before, it is

{Moon.Age() At Sun.Set(Lct) On PreviousDay}

Now for the clarification why Age is called instead of Phase. The values returned by Phase do not wrap around at 0 like the ones returned by Age do. Phase returns a percent value from 0% to 100%, indicating the illuminated portion of the visible disc. The returned value oscillates between 0% and 100%. This makes it impossible to decide whether e.g. 99% means a full moon has recently occurred or that one is about to occur.

The Age function returns the age of the Moon in degrees, in the range [0..360[. When two instants straddle a full moon, the age at the first instant will be somewhat below 360°, whereas the age at the second instant will be somewhat above 0°. Thus, we can detect a full moon as follows:

{Moon.Age() At Sun.Set(Lct) On PreviousDay} < 180 And
{Moon.Age() At Sun.Rise(Lct)} > 180

The entire StellarScript expression so far becomes

AngleDiff(Sun.Lon(Equ), 180) >>= 180 And
WeekDay() == Wednesday And
{Moon.Age() At Sun.Set(Lct) On PreviousDay} < 180 And
{Moon.Age() At Sun.Rise(Lct)} > 180

Sunrise and Sunset

The concept of contexts also solves the problem we ignored earlier on: We need to make sure spring equinox occurs between sunrise and sunset. With the ability to define new contexts, this is simply

{Sun.Lon(Equ) At Sun.Rise(Lct)} > 180 And
{Sun.Lon(Equ) At Sun.Set(Lct)} < 180 And
WeekDay() == Wednesday And
{Moon.Age() At Sun.Rise(Lct)} > 180 And
{Moon.Age() At Sun.Set(Lct) On PreviousDay} < 180


This finally gives us what we need. Searching from 1.1.1600 to 31.12.3000, we find, for example:

Vienna London
21.3.2239 20.3.2182
20.3.2459 20.3.2554

The results depend on the observer’s geographical location, since the times of sunrise and sunset are involved.

Of course it is desirable to verify these results. I have yet to find a reliable source that gives the data involved (the Sun’s longitude, the Sun’s rising and setting time, the age of the Moon) so far in advance. However, for time intervals that can be verified, the times of sunrise and sunset calculated by Stellaris are usually accurate to one minute.

Speeding It Up

The search as given above takes 3m19s on my current computer. There are several ways we can improve the speed.

Since StellarScript expressions make use of short-circuiting, placing faster checks first increases efficiency. Writing the expression as

WeekDay() == Wednesday And
{Sun.Lon(Equ) At Sun.Rise(Lct)} > 180 And
{Sun.Lon(Equ) At Sun.Set(Lct)} < 180 And
{Moon.Age() At Sun.Rise(Lct)} > 180 And
{Moon.Age() At Sun.Set(Lct) On PreviousDay} < 180

reduces the execution time to 42s.

Since we know spring equinox always occurs on March 20 or 21, we can avoid performing calculations for other dates and examine, say, March 19 through March 22 to be on the safe side.

The StellarScript expression then finally becomes

WeekDay() == Wednesday And
Day() >= 19 And Day() <= 22 And
{Sun.Lon(Equ) At Sun.Rise(Lct)} > 180 And
{Sun.Lon(Equ) At Sun.Set(Lct)} < 180 And
{Moon.Age() At Sun.Rise(Lct)} > 180 And
{Moon.Age() At Sun.Set(Lct) On PreviousDay} < 180

Expressed this way, the search takes 21s on my current computer.


Stellaris on Facebook