--- title: "Guide for AI agents" description: > Conventions, failure behaviour and pitfalls an AI agent (or anyone scripting radiatR) needs before analysing directional data. output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Guide for AI agents} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(radiatR) ``` This guide is written for an AI agent asked to analyse directional data with radiatR, and for the person checking its work. It states the conventions the package uses, how it fails, and the mistakes that are easy to make. Every code chunk below is run when the package is built. ## Two ways in Start from **trajectories** when you have tracking output (x/y positions over time) and need one heading per track, or from a **table of angles** when the headings have already been measured. ```{r tracks} sim <- as.data.frame(simulate_tracks(seed = 1, n_points = 50)) path <- tempfile(fileext = ".csv") write.csv(sim, path, row.names = FALSE) ts <- read_tracks( path, mapping = list(id = "trial_id", time = "frame", x = "abs_x", y = "abs_y"), time_type = "frames", fps = 30, keep = "condition" ) hd <- derive_headings(ts, rule = "distal", carry = "condition") head(hd, 3) ``` ```{r angles} bearings <- data.frame(bearing = c(10, 20, 350, 5), site = c("a", "a", "b", "b")) hf <- headings_frame(bearings, col = "bearing", units = "degrees", angle_convention = "clock") hf ``` ## Angle conventions - Internally every angle is in **radians** in the **unit-circle convention**: 0 points East and angles increase counter-clockwise. Headings derived from tracks lie in [0, 2*pi). - `headings_frame()` converts on the way in. `units` has no default. `angle_convention = "clock"` means 0 = North, increasing clockwise (compass bearings); `"unit_circle"` means the data are already 0 = East, counter-clockwise. In the example above a bearing of 10 degrees is stored as `r round(hf$bearing[1], 3)` radians. - `headings_frame()` does not wrap values supplied in the unit-circle convention: 370 degrees is stored as `r round(370 * pi / 180, 2)` radians and -20 degrees as `r round(-20 * pi / 180, 2)`. Wrap them yourself if a later step needs [0, 2*pi). - Summary tables carry two columns. `mean_dir` is in radians, unit-circle convention. `mean_dir_deg` is in the **display convention** set by `circ_display()`, which by default is 0 = North, clockwise. It is therefore not `mean_dir * 180 / pi`. ```{r summary} circ_summarise(hd, col = "heading", units = "radians", .by = "condition") ``` State the convention whenever you report a direction. ## Coordinates and calibration Heading rules that use a radius (`crossing`, `exit`, `entry`, `ring_tangent`, `distal` with `max_radius`) read radii in the units of the coordinates the `Tracks` object holds. Decide how those coordinates relate to the arena before choosing a rule. - `normalize_xy = FALSE` (the default) keeps x/y as supplied. - `origin = c(x, y)` with `radius` is **calibration**: every trajectory is mapped with `(xy - origin) / radius`, so the arena edge is at radius 1 and bearings from the arena centre are preserved. Supply both together. - `normalize_xy = TRUE` rescales each trajectory on its own bounding box. It preserves shape only and does **not** preserve bearing from a fixed arena centre. It is not calibration and cannot be combined with `origin`/`radius`. ## Map every column yourself - Without a dialect, `read_tracks()` keeps only the mapped columns and those named in `keep` (a dialect keeps the columns it produces). Name every grouping column you will need later in `keep`, then bring it onto the headings with `derive_headings(carry = )`. A group comparison on a column that was not kept fails with "column ... not found". - A name in `keep` or `carry` that is not in the data is dropped without a message. Check the result has the columns you asked for. - A `carry` name that is already a column of the result (`id`, `time`, `heading`) is an error: the carried values would overwrite the derived ones. - A role missing from `mapping` is guessed from the column names. An unmapped column called `theta`, `angle`, `phi`, `bearing`, `deg` or `degrees` becomes the angle, read in radians unless `angle_unit` says otherwise; one called `w`, `weight` or `weights` becomes the weight. Map `id`, `time`, `x` and `y` explicitly, and check `guess_columns()` on the table before loading. ## Time - `time_type` says how the time column is expressed: `"frames"`, `"seconds"` or `"posix"`. Prefer stating it to relying on `"auto"`. - `time_type = "frames"` requires `fps`. The frame rate is stored on the `Tracks` object and used by `elapsed_seconds()` and the kinematics functions. - radiatR never assumes a frame rate. Supply `fps`, or set it afterwards with `set_frame_rate()`. ## Failures are information radiatR stops rather than guessing. Read the error; do not work around it. | Condition class | Meaning | |---|---| | `radiatR_invalid_rows` | x/y missing or non-finite; the error lists rows and ids | | `radiatR_malformed_values` | a column holds values that are not numeric; lists rows and values | | `radiatR_invalid_tracks` | the data break a `Tracks` invariant (see `?tracks`) | | `radiatR_nonestimable` | a statistic cannot be estimated for a group | | `radiatR_dropped_rows` | warning raised when `on_invalid = "drop"` removed rows | | `radiatR_attrition` | message from the `deeplabcut`, `sleap`, `ethovision` and `anymaze` dialects: the points masked at load and the rows left without a position (kept in `@meta$attrition`; not the heading attrition of the next section) | | `radiatR_dialect_error` | a dialect cannot read the file as that program's output; the message says why | - `on_invalid = "error"` is the default. `"drop"` removes rows with missing or non-finite x/y and warns; it is a decision about the data, so report it. - When the same id appears in more than one file, the default `id_collision = "error"` stops. `"namespace"` prefixes each id with its file's name (less extension; the full name or the end of the path where files share that). ## Heading rules and attrition `derive_headings()` returns one row per track (some rules can return more). When a rule yields no heading for a track, the row is kept with `NA` and the result carries `n_total` and `n_missing` (headings, i.e. rows), `n_trials` (distinct trials) and `missing_ids` (each trial with a missing heading, once) attributes. `on_missing` controls what else happens: `"warn"` (the default) raises a warning, `"error"` stops, `"quiet"` says nothing. The `ransac_straight` rule draws random numbers, so call `set.seed()` before it. ```{r attrition} attr(hd, "n_total") attr(hd, "n_missing") ``` Attrition is often not random: a track that never reaches a ring has no `crossing` heading, and those animals may differ from the rest. Report `n_missing` alongside any statistic, and do not switch rules silently to recover the missing tracks. ## Tests ```{r tests} set.seed(1) test_uniformity(hd, group_col = "condition", angle_col = "heading") test_mean_directions(hd, group_col = "condition", angle_col = "heading") ``` - `test_uniformity()` asks whether headings are uniform around the circle; `test = "vtest"` tests against an expected direction `mu`, in radians. - `test_mean_directions()`, `test_concentration()` and `test_distributions()` compare groups. - Pass `axial = TRUE` for bidirectional data (an axis, not a direction). - Monte Carlo and permutation p-values, and the bootstrap functions, use R's random number generator. Call `set.seed()` first and report the seed. ## Pitfalls 1. Reporting `mean_dir_deg` as if it were counter-clockwise from East. 2. Treating `normalize_xy = TRUE` as calibration. 3. Forgetting `keep` / `carry`, or misspelling a name in them, then finding the grouping column is gone. 4. Using `on_invalid = "drop"` without saying how many rows were dropped. 5. Ignoring `n_missing` after `derive_headings()`. 6. Giving radii to a heading rule in the wrong units. 7. Reporting a resampling p-value without a seed. 8. Catching an error and carrying on with a different method. 9. Letting `read_tracks()` guess an angle or weight column from its name.