Skip to content

Time

Basic Types

Within the ReSim libraries, we generally represent durations using resim::time::Duration, which is an alias for std::chrono::nanoseconds and timestamps as resim::time::Timestamp which is an alias for std::chrono::sys_time<resim::time::Duration>.

Converters

Floating Point Representation

While we generally use the aforementioned integer-based timestamps and durations where possible, it is very often convenient to store times as double-precision floating point values, especially when using them in physical computations. If the values are sufficiently small (e.g. elapsed time in a sim), this can be done with no loss of accuracy. To facilitate, this, we have convenience converters:

#include "resim/time/timestamp.hh"
#include "resim/assert/assert.hh"

// ...

using namespace resim::time;
Duration my_duration{std::chrono::nanoseconds(10000U)};

double my_duration_s = as_seconds(my_duration);

// Should pass since my_duration is small
REASSERT(as_duration(my_duration_s) == my_duration);

my_duration += std::chrono::system_clock::now().time_since_epoch();

my_duration_s = as_seconds(my_duration);

// Will likely not pass because my_duration is big and precision is lost
// converting it to a double.
REASSERT(as_duration(my_duration_s) == my_duration);

Seconds & Nanoseconds Struct Representation

It's common in time serialization formats (e.g. google::protobuf::Timestamp and ROS2's builtin_interfaces/msg/Time.msg) for the seconds and nanoseconds to be stored as separate integer counts. For google::protobuf::Timestamp, we provide standard packers and unpackers:

const time::Timestamp time{};
google::protobuf::Timestamp time_msg{};

pack(time, &time_msg);

const time::Timestamp unpacked_time = unpack(time_msg);

To facilitate other conversions to and from such serialization types, we have a time representation called SecsAndNanos:

struct SecsAndNanos {
  int64_t secs = 0;
  int32_t nanos = 0;
};

And converters to and from it:

const SecsAndNanos my_secs_and_nanos = to_seconds_and_nanos(my_duration);
REASSERT(from_seconds_and_nanos(my_secs_and_nanos) == my_duration);

Event Scheduling

Running simulations or processing logs often requires iterating through an ordered set of times in increasing order. These sets of interesting timestamps are not always uniformly spaced, and sometimes we want to add future times to this set as we are iterating through it. For instance, when modeling message transmission latency in a simulated system (i.e. one where we are explicitly controlling a simulated time), we may want to schedule a future time when we expect the message to arrive so we can properly transmit it to its receiver and keep the order of messages consistent with the real world system. To support this functionality, we define the EventSchedule class template which can store a time-ordered queue of events with arbitrary payloads. Here's a simple example of how to use it:

#include <iostream>

#include "resim/time/event_schedule.hh"

// ...

EventSchedule<std::string> my_messages;
my_messages.schedule_event(Timestamp(std::chrono::nanoseconds(200)), "ReSim");
my_messages.schedule_event(Timestamp(std::chrono::nanoseconds(100)), "Hello");
my_messages.schedule_event(Timestamp(std::chrono::nanoseconds(200)), "user!");

std::cout.precision(9);
std::cout << std::fixed;
while (my_messages.size() > 0U) {
  const auto event = my_messages.top_event();
  std::cout << "[" << event.time.time_since_epoch().count() / 1e9 << "] "
            << event.payload << std::endl;
  my_messages.pop_event();
}

Note that the event schedule is "stable" in the sense that it's first-in-first-out for events with the same timestamp. Hence the code above will print out:

[0.000000100] Hello
[0.000000200] ReSim
[0.000000200] user!

Interval Sampling

Another common operation in running simulations and collecing log metrics is sampling an interval uniformly. For example, one may want to numerically compute a robot's average deviation from its desired pose over the course of a simulation. If a user wants to compute this average with Reimann rectangles, they could pick a maximum rectangle width (dt) that they are willing to accept (depending on what accuracy they desire) and then compute the integral using resim::time::sample_interval() and the associated resim::time::num_samples() function like so:

#include <cmath>

#include "resim/time/sample_interval.hh"

// ...

const Timestamp start_time;
const Timestamp end_time{start_time + std::chrono::seconds(30)};
const Duration max_dt = std::chrono::microseconds(100);

// A stand in function for illustration purposes.
const auto deviation = [](const Timestamp &t) {
  return std::cos(std::sqrt(as_seconds(t.time_since_epoch())));
};

double integral = 0.;
sample_interval(start_time, end_time, max_dt, [&](const Timestamp &t) {
  // Left rectangles so we leave off the value at the end time
  if (t != end_time) {
    integral += deviation(t);
  }
});

// The actual dt per rectangle is the total interval divided by (N - 1)
const int N = num_samples(start_time, end_time, max_dt);
const double dt_s = as_seconds(end_time - start_time) / (N - 1);
integral *= dt_s;

// Make sure we match the analytical result
const double analytical_result = 2. * (-1. + std::cos(std::sqrt(30.)) +
                                       std::sqrt(30.) * std::sin(std::sqrt(30.)));
REASSERT(std::fabs(integral - analytical_result) < 1e-4);

Note

Feel free to play around with the source code for the examples above.