-
Notifications
You must be signed in to change notification settings - Fork 20
Coordinate Systems
GAMS' coordinate system capabilities, contained within the
gams::pose
namespace, are designed to provide easy conversion between different kinds of
coordinate reference frames, embedded within each other. For example, you can
represent the Earth using a GPS reference frame, and embed Cartesian frames
within it throughout the world, with origins at given latitudes, longitudes,
and altidues. This would allow for manipulating positions on a scale of meters
near that origin, automatically translating back to latitude and longitudes as
needed, as well as seemlessly translating between points in the various
embedded frames.
In addition, Cartesian frames can embed within each other, forming trees of reference frames, equivalent to transform trees in ROS.
The basic coordinate types,
Position,
Orientation,
and
Pose
(which combines position and orientation information into one object) bind to a
reference frame. GAMS provides a default Cartesian reference frame, suitable if
your application does not require any conversion between reference frames.
- Introduction
- Reference Frames
- Coordinate Types
- Stamped Coordinates
- Coordinate Operations
- Platform Frames
- Frame Timestamping
- Frame Tree Storage/Buffering
- Frame Storage Location
- Frame Expiration
- FrameStore
- Producing Frames
ReferenceFrame objects represent the various coordinate systems coordinates can
belong to. Reference frames can embed within other frames, allowing for
transformations between those frames. ReferenceFrames are reference counted
to ensure they live as long as any child frames, or coordinates using them.
A ReferenceFrame has an ID. This ID can be specified at time of creation;
otherwise, it will receive a random unique ID. Either way, this ID should be
unique across all KnowledgeBases a process might save ReferenceFrames to.
Random IDs are generated lazily, as needed. This saves the cost of generating
one for short lived temporary frames. Call the has_id() method to determine
if a ReferenceFrame already has an ID; calling id() will generate one
automatically.
GAMS supports two kinds of reference frames: Cartesian, and GPS. Cartesian is the default, and will be used if no other kind is specified.
The GPS frame is a simple spherical model, and provides position coordinates as Latitude, Longitude and Altitude.
ReferenceFrames have string ID associated with them. You can specify one at
time of construction, as the first parameter of the constructor. If you omit
the ID, a random GUID will be generated.
There are two default frames available, each accessed through a global function
in the pose namespace: default_frame() and gps_frame(), a Cartesian and
and GPS type respectively. The former is also the frame used if a coordinate
is constructed without an explicit frame.
These frames are parentless, and may serve as roots of a frame tree. A frame may have a parent, by specifying an origin point within another frame. By creating frame objects with parents, coordinates can easily convert between any frames in the resulting tree. For example:
using namespace gams::pose;
// Latitude/Longitude frame representing planet Earth
const ReferenceFrame &earth_frame = gps_frame();
// Cartesian frame representing a house, at the given Latitude and Longitude
ReferenceFrame house_frame("House", Position(earth_frame, 40.4464481, -79.9500588));
// Cartesian frame representing a given room in the house, centered at
// 10 meters East and 20 meters North of the above point
ReferenceFrame room_frame("Room", Position(house_frame, 10, 20));
In this example, coordinates within room_frame may be converted to coordinates
with earth_frame. Instead of using Position, Pose may be used instead to
specify frames that are rotated with respect to another. Position objects keep a
reference to their frame, so you must ensure that a frame object outlives any
positions belonging to it.
There are additional reference frame related types, including ReferenceFrameVersion,
ReferenceFrameIdentity, and ReferenceFrameType. These are implementation details,
hidden behind the ReferenceFrame facade. Users should not reference these directly,
except to compare the return value of ReferenceFrame::type() to either pose::Cartesian
or pose::GPS.
GAMS supports nine coordinate types; three fixed vector:
Position,
Orientation,
and
Pose.
And two kinds of free vectors, with three levels of derivatives each:
Displacement,
Velocity, and
Acceleration;
Rotation,
AngularVelocity, and
AngularAcceleration.
All the above coordinates bind to a frame (the first argument of any constructor). If you construct a coordinate of any type omitting the frame argument, it will bind to the default reference frame, a Cartesian frame that has no parent.
There are frame-less versions of the above, which have the same name, but appended with Vector, e.g., PositionVector. If a framed coordinate is constructed from a frame-less coordinate, the frame-less one is treated as though it already belongs to the framed coordinate's own frame.
There a few base classes that the above types inherit from. These are implementation details, and are not relevant to most users of GAMS directly.
Position represents position information. It can represent coordinates within
any type of 3-dimensional coordinate system (there aren't separate classes for
GPS vs Cartesian coordinates). Position provides methods for accessing those
coordinates according to various naming conventions:
Position earth_loc(earth_frame, 40.4465197, -79.9494575);
earth_loc.lat(); // Latitude (40.4465197)
earth_loc.lng(); // Longitude (-79.9494575)
earth_loc.alt(); // altitude (0 meters above surface, the default)
Position room_loc(room_frame, 1, 2);
room_loc.x(); // x-coordinate (1 meter from origin)
room_loc.y(); // y-coordinate (2 meters from origin)
room_loc.z(); // z-coordinate (0 meters above origin, the default)
These different functions are for the sake of readable code; there is no
underlying difference between, for example, earth_loc.lat() and
earth_loc.x().
To set coordinates, simply pass the new value as an argument to the function for
that coordinate; e.g., room_loc.z(2).
The Velocity and Acceleration classes expose exactly the same interface as
Position.
The Orientation class represents a rotation or orientation relative to its
frame. There are two primary ways to construct an Orientation. The easiest is
to specify one of the fundamental axes, and an Orientation (in degrees,
counter clockwise if looking from above) around that axis:
Orientation simple_rot(room_frame, Orientation::Z_axis, 90);
The more expressive form represents the orientation in an angle-axis vector
format. For this, the rx, ry, and rz parameters represent a 3-vector which
points in the direction of the axis of rotation relative to the origin
orientation of the frame (e.g., for GPSFrame, due north along the ground), and
whose magnitude is the amount of rotation about that axis in radians. The above
orientation could be specified instead as follows:
Orientation simple_rot(room_frame, 0, 0, M_PI / 2);
This notation can represent any possible orientation relative to the frame, while the first notation is limited to simple orientations.
The AngularVelocity and AngularAcceleration classes expose exactly the same
interface as Orientation.
The Pose class combines both position and orientation information. It may be
constructed as follows:
Pose room_pose(room_frame, 3, 1, 0, 0, 0, M_PI / 2);
Where the positional 3-vector is specified first, followed by the rotational 3-vector. A variety of other constructors are avialable (see the Doxygen documentation).
Each coordinate type has a timestamped version, prefixed with Stamped. These
versions support the same constructors that the un-stamped versions have, but
with an optional initial timestamp argument, a std::chrono::time_point, available
as gams::pose::TimeValue. These types provide three getter/setter pairs, all operating
on the same internal timestamp, but with different units:
-
time()gets the currentTimeValue,time(TimeValue)sets it -
nanos()gets the timestamp in nanoseconds,nanos(uint64_t)sets it -
secs()gets the timestamp in seconds (as a double),secs(double)sets it
GAMS provides the following methods on all coordinates (Position,
Orientaion, and Pose, as well as the derivative coordinates):
transform_to creates a copy of a coordinate, but in a new reference frame.
This new frame must somehow be related to the coordinate's current frame.
Pose gps_pose = room_pose.transform_to(earth_frame);
transform_this_to transforms the coordinate object, in place, to the new
reference frame. Again, it must be related to the current frame.
room_pose.transform_to(earth_frame);
distance_to finds the distance between the two coordinates. For Position and
Pose, this is the number of meters between them. For Orientation, this is the
number of degrees between them. Note that the coordinates need not be in the
same reference frame, but they must be transformable to a common reference
frame. Orientation and Pose also provide angle_to. For Orientation, this
is synonymous with distance_to. For Poses, this finds the degrees between the
orientation parts of the Poses.
room_loc.distance_to(earth_loc); // returns sqrt(5)
These operations can raise exceptions. The most common is the
unrelated_frames exception, which indicates that the coordinates you are
operating on belong to frames which are not connected by a tree. exception,
which indicates that the coordinates you are operating on belong to frames
which are not connected by a tree. The other possible exception is
undefined_transform, raised if the transform tree structure is not
supported. In particular, the GPS frame should not be embedded within a
Cartesian frame. This will be avoided as long as you always use gps_frame()
and never construct your own GPS-type ReferenceFrame. Both exceptions
derive from std::exception.
In GAMS, every platform belongs to a reference frame. By default, this frame is the Earth GPS-based frame. In most cases, you will manipulate coordinates belonging to this frame, or to frames embedded in it. For example:
class MyAlgorithm : public BaseAlgorithm {
...
/* Note: BaseAlgorithm provides:
protected: BasePlatform *platform_; */
void execute() {
const ReferenceFrame &platform_frame = platform_->get_frame();
Position platform_location = platform_->get_location();
ReferenceFrame local_frame(platform_location) // Create Cartesian frame
// centered on the platforms current location
Pose target(local_frame, 5, 0); // location 5 meters east of platform
Pose target_gps(platform_frame, target); // convert to platform frame (GPS)
platform_->move(target); // conversion to platform's frame is automatic;
// equivalent to platform_->move(target_gps);
double distance = target.distance_to(platform_location);
// distance == 5
}
...
}
Additionally, platforms which target the VREP simulator (derived from
VREPBase) provide a get_vrep_frame() method which returns a Cartesian type
ReferenceFrame, embedded within the platform's GPSFrame, which corresponds
to the reference frame inside VREP. This frame will be a child frame of the
platform's frame.
In general, any coordinates you pass to platform methods must belong to that platform's frame, or to a frame that is a child of that frame (or grandchild, etc.).
When you implement a Platform, you can choose what frame you want the platform
to view the world through, by overriding the get_frame() method. For example:
class MyPlatform : public BasePlatform {
...
ReferenceFrame get_frame() const override {
const KnowledgeBase *kb = get_knowledge_base();
double origin_lat = kb.get(".building_origin_lat").to_double();
double origin_lng = kb.get(".building_origin_lng").to_double();
Position origin{gps_frame(), origin_lng, origin_lat, 0};
return ReferenceFrame("BuildingFrame", origin);
}
...
}
In this example, the platform is designed to operate within a given building,
so it is natural to use a local cartesian frame centered on it as the base
frame. Note that get_frame() should provide a global frame. It should not
move along with the platform itself.
Alternatively, the above platform could simply return default_frame() instead
of constructing a new Cartesian frame embedded in the GPS frame. This would
lose the capability to convert local coordinates to GPS coordinates, or to
transform to other local cartesian frames elsewhere in the world.
Reference frames provide an additional field not yet discussed: a timestamp. Each frame can have a timestamp, which represents the moment in time when the frame was measured or interpolated to. It is the moment best estimated as when the transformation provided by the frame is accurate.
You can set the timestamp of a frame at construction, by passing an integer value after the origin pose. If none is given, a default of -1 is used, which causes the frame to be "eternal". It is treated as always accurate at all possible timestamps. This is appropriate for, for example, a building frame, which will presumably not change during the course of a mission.
Frame timestamps are not checked during transform operations. It is assumed
that if your frame tree has inconsistent timestamps, they are within an
appropriate tolerance. If you are using timestamps, you should use the
load_tree method, described in the next section, to ensure you are operating
on trees with fully consistent timestamps.
When initially constructed, ReferenceFrame objects are strictly in-memory
structures. They have no connection to a KnowledgeBase. You can use the
save method to store a ReferenceFrame to the KnowledgeBase. The frame
will be stored based on its ID and version for later lookup with load, which
loads a given ReferenceFrame. For example:
// These functions not provided by GAMS
double get_sensed_x();
double get_sensed_x();
uint64_t get_timestamp();
...
double x = get_sensed_x(), y = get_sensed_y();
uint64_t time = get_timestamp();
ReferenceFrame sensor_frame("Sensor", Position(x, y), time);
sensor_frame.save(kb);
...
ReferenceFrame sensor_frame = ReferenceFrame::load(kb, "Sensor", time);
If the requested frame name, or the requested time, do not exist, load
will throw ReferenceFrameException.
Usually, however, you'll want to load an entire consistent tree from the
KnowledgeBase. For this, you can use the load_tree method. For example:
std::vector<std::string> ids = {"Building", "Sensor", "FrontCamera"};
std::vector<ReferenceFrame> frames = ReferenceFrame::load_tree(kb, ids, time);
std::vector<ReferenceFrame> latest_frames = ReferenceFrame::load_tree(kb, ids);
In this example, frames will contain the given frame IDs at the given time,
load them into ReferenceFrame objects, interpolated as necessary,
and return them (in the same order as the ID list given). latest_frames will
contain the latest possible consistent set of frames.
load_tree does not only examine and load the specific IDs given. It will also
consider any intermediate frames needed to join the IDs given into a tree,
and load/interpolate those as well. It will not return those frames directly
as part of the returned vector.
If load_tree cannot find a consistent tree which satisfies the timestamp
given, or if the frames given are not part of the same frame tree (no path
through parent frames can transform between them), or if any of the IDs given
do not exist in the knowledge base, it will throw a ReferenceFrameException.
Note that save only saves a single frame, not an entire tree. Ensure you are
saving the entire ancestry of frames you need.
The gps_frame() and default_frame() frames are not saved by default.
You will need to save them yourself, if they are in the ancestry of any
frames you wish to save, to each KnowledgeBase where you wish to save
such frames. This should typically be done at the start of your controller.
By default, all frames are stored with a ".gams.frames" prefix in the
KnowledgeBase. This keeps them in a well-known location for later
loading. This prefix also implies that frames are kept local, and not
sent to other agents. To override this prefix, each load and save function
can take a prefix argument as final argument. You must pass the same
prefix to save and load/load_tree. Load operations will only look at
a single prefix, so each prefix must hold complete trees, including any
default frames, to be useful. If you save frames without a "." at that start,
the frame data will be sent to other agents, if the KnowledgeBase is configured
with transports appropriately.
By default, all saved frames with all timestamps are stored in the
KnowledgeBase indefinitely. By setting frame expiry, old frames will be
cleaned out as new ones are saved. You can set expiry in three ways:
- Pass the expiry age as a second parameter to
ReferenceFrame::save() - Set the expiry age for a frame ID, by calling
ReferenceFrame::expiry()on a frame of that ID. - Set a global default expiry, by calling
ReferenceFrame::default_expiry(). This global default will apply to all new frame IDs, but not those that already exist.
When an expiry applies to a save operation, from any of the above (precedence
going to first on the list), any frames older than the given age relative
to the frame being saved will be removed from the KnowledgeBase. An expiry
of -1, the default, will not expire any frames. If a frame is saved with
timestamp -1, and expiry other than -1, all other copies of that frame ID will
be deleted.
Note that deleting entries from the KnowledgeBase can be dangerous. The
ReferenceFrame interface provided by GAMS ensures safety for accesses through
itself, but if you access the variables directly vi Madara, be sure no
outstanding VariableReference or Madara containers exist that reference the
underlying Madara variables of the expiring ReferenceFrame.
The FrameStore object encapsulates the information needed to load and store
frames, providing a minimal interface, in-place of the direct KnowledgeBase
used in the interface described above. It is constructed with a KnowledgeBase
value, optional prefix, and optional expiry. Call load/load_tree/save on
the FrameStore object to use the saved values.
Some threads might only need to produce ReferenceFrame objects and store
them to the KnowledgeBase, such as a sensor polling thread. Such threads
do not need to load any frames from the KnowledgeBase. You still need
to specify the correct parent frame when saving a frame, though, but this
parent frame need only have the correct ID. For example, suppose an encoder
for a wheel sensor produces the frame id "LeftFrontWheel", with parent frame
"LeftFrontAxle":
// These functions not provided by GAMS
double get_left front encoder();
uint64_t get_timestamp();
...
double angle = get_left_front_encoder();
uint64_t time = get_timestamp();
ReferenceFrame parent("LeftFrontAxle", Pose()); // fake parent frame
ReferenceFrame me("LeftFrontWheel", Orientation(parent, 0, 0, angle), time);
me.save(kb);
It doesn't matter what the parent's frame actual ancestry is, or what its timestamp is, or its relationship to its parent, as long as you don't need to save the parent itself.