Scanning GCN events for optical counterparts#
SkyPortal can crossmatch broker alerts against the localization of a GCN event
and raise whatever falls inside it as candidates to scan. This works for any
event with a localization, e.g. Einstein Probe and Swift XRT
error circles, Fermi and GW skymaps, against any broker implementing
query_alerts (BOOM, Babamul, ALeRCE, Fink, Lasair, …).
How it works#
The gcn_crossmatch service walks every event still inside its active window.
For each one, and for each active broker:
The localization is reduced to a search cone. Cone localizations (EP, Swift XRT) give this directly; skymaps are bounded using the stored 90% credible contour. A localization that cannot be bounded, or whose bound exceeds
max_radius_deg, is skipped and logged — never guessed at.The broker is queried inside that cone, restricted to an alert-JD window around the event, with quality cuts applied broker-side (see below).
Returned alerts are checked for real containment against the localization’s HEALPix tiles. As the cone over-selects, this decides candidate membership.
Survivors become an
Objplus aCandidateagainst the configured filter, so they appear on the scanning page.Each match is annotated with how it relates to the event (below).
Events are re-queried while they stay active, because alerts keep arriving after the trigger. Each pass resumes from the newest alert already seen rather than from wall-clock time, so late-arriving alerts are not skipped.
A one-shot archival pass also searches the window before the event. Those
alerts cannot have been caused by it, so they rule a candidate out: a position
already flaring last month is a variable, not a counterpart. Such matches carry
prior_activity: true on the annotation.
Configuring the quality-cut filter#
Without cuts, every artifact, asteroid and variable star inside the error region is reported. The cuts are expressed as a broker filter, so they are versioned and editable in the filter builder rather than frozen in code.
Create a filter on the broker (for BOOM, an aggregation pipeline) holding the cuts you want. A reasonable ZTF starting point is in
ZTF_QUALITY_CUTSinskyportal/utils/gcn_crossmatch.py: real/bogus thresholds (rb,drb), positive subtractions only, solar-system rejection, stellar rejection bysgscore/distpsnr, and a PS1 red-star colour cut.Create the corresponding SkyPortal
Filter, whosealtdatacarries{"boom": {"filter_id": "<broker filter uuid>"}}.Point the service at it:
gcn_crossmatch:
enabled: True
filter_id: 42 # the SkyPortal Filter id
The service reads that filter’s active version and prepends the cone to it,
so cuts run on the broker and only survivors cross the wire. With filter_id
unset it falls back to the built-in cuts, which works but is frozen — and
without a filter no Candidate can be created, so nothing reaches the scanning
page.
Choose the filter’s group deliberately. A candidate is visible to the filter’s group. Annotations inherit the event’s groups, so the link between an object and a restricted event stays restricted, but the candidate itself does not. For a proprietary stream, use a filter whose group matches that stream’s audience.
What gets crossmatched#
A Filter is one crossmatch configuration. It already names the broker, the
stream (hence the survey and the alert programs) and the group that sees the
candidates, so nothing about which alerts to search lives in config.yaml.
Opt a filter in through its altdata:
altdata:
gcn_crossmatch:
enabled: true
# optional: restrict to matching events. Same shape as DefaultGcnTag --
# an absent or empty list means no restriction.
filters:
gcn_tags: ["Einstein Probe"]
# optional: any setting from the table below, for this filter alone
delta_t_after: 7.0
Adding a second survey is a second filter, not a code change: point it at an LSST stream and the matching broker, and it runs alongside with its own windows, its own cuts and its own audience.
Progress is tracked per (event, filter), so two filters sharing a broker never interfere.
Configuration reference#
These are the defaults every opted-in filter inherits, under gcn_crossmatch
in config.yaml. Any of them can be overridden per filter.
| Setting | Default | Meaning |
|—|—|—|
| enabled | False | Run the service at all |
| poll_interval | 300 | Seconds between passes |
| max_event_age | 31.0 | Days; older events are no longer crossmatched |
| recheck_interval_minutes | 60.0 | Minimum gap before re-querying an event |
| delta_t_before / delta_t_after | 1.0 / 31.0 | Query window around the event, in days |
| archival / archival_days | True / 31.0 | One-shot pre-event search |
| max_radius_deg | 5.0 | Skip localizations bounding wider than this |
| credible_level | 90 | Contour used to bound non-cone localizations |
| cumprob | 0.95 | Cumulative probability defining “inside” |
| max_alerts | 500 | Cap per event, per broker, per pass |
Scanning the results#
Matches appear on the candidates page under the configured filter, like any
other candidates. Each carries an annotation with origin GCN-crossmatch, keyed
by event, holding the fields the reviewer needs:
| Field | Meaning |
|—|—|
| delta_t | Days between the event and the alert |
| distance_arcmin | Separation from the localization centre |
| distance_ratio | That separation as a fraction of the error radius |
| age | Days since the object’s first detection |
| drb | Deep-learning real/bogus score |
| sgscore, distpsnr | Star/galaxy score and distance to the nearest PS1 source |
| ssdistnr, ssmagnr | Proximity to a known solar-system object |
| ndethist | Number of prior detections |
| event_mjd | Event time, MJD |
| prior_activity | Set when the position was already active before the event |
| dist_mean, dist_std | For a 3D (GW) skymap: the distance the event implies at this position, in Mpc |
Because these are annotations, they can be sorted and filtered on the scanning
page — delta_t and distance_arcmin are the usual first cut, and
prior_activity is the quickest way to discard variables.
dist_mean/dist_std are the conditional distance at the candidate’s own
pixel, not the skymap’s marginal distance: for a localization spanning a range
of distances the two differ, and “inside the localization volume” means the
former. They are recorded rather than cut on, because an alert rarely has a host
redshift at discovery — compare them against one when it is known.
Once a candidate is saved to a group, it appears as a source on that event’s page in the usual way.
Per-event progress and requeuing#
GET /api/gcn_event/{dateobs}/crossmatch
POST /api/gcn_event/{dateobs}/crossmatch # requires Manage GCNs
GET returns per-broker progress: when the event was last queried, how far
through the alert stream it has got, how many matches it has produced, and any
error. POST resets that state so the next pass re-queries from the start of
the window, archival pass included; you can use it after changing the filter or the
search parameters.
Finding the events#
The gcn_events page filters by group, so a proprietary stream can be separated from public alerts. Group filtering narrows within what you can already read; it never widens access.