radiatR 0.1.3
Breaking changes
A semicolon-separated file whose decimal point was sniffed from a column
read_tracks()does not read for a role (a label1.5), while its x, y, time, angle or weight columns hold only values that could be digit grouping (1.234), now stops withradiatR_ambiguous_decimalinstead of loading values that may be wrong by a factor of 1000.read_opts = list(decimal = ".")loads it as before. Files whose role columns show a decimal point (a time column0.040) are unaffected. The same applies to EthoVision exports that are not comma-separated, and to the other dialects that sniff the mark. A;file whose decimal sniff finds a point, and whose first data row’s quoted field runs past the first 20 lines, now needs the mark given inread_opts.read_tracks(read_opts = )now reaches the built-in dialects, which used to ignore it: every dialect takesdecimal; the.read_any()-based ones (trackmate,trex,sleap,idtrackerai_wide) alsodelimandext(text files) andsheet(Excel files);ethovisionandanymazedelim. An option a dialect cannot use or that does not fit the file’s type (delimordecimalfor an Excel file,sheetfor a text file), or any option for a registered dialect without aread_optsargument, now stops withradiatR_invalid_argumentinstead of being silently ignored.read_optsentries are checked on every path: an unknown name (which$used to match partially:decimals = ","was read asdecimal) or a decimal mark other than"."/","stops, as does a name given twice. Every refusal carries the offending entry asoption. The decimal-mark hint on malformed values now applies to dialects and toH:MM:SSdurations, no longer to Excel files, and the error carriesdecimalandsuggested_decimal.read_tracks()without a dialect now refuses aread_optsentry that does not fit the file’s type, as dialects do:delimordecimalfor an Excel or Arrow file,sheetfor a text or Arrow file (Arrow files were also let through by the dialects). A folder of mixed text and Arrow files read withread_tracks_dir(read_opts = list(delim = ))therefore stops. For a data frame,read_optstakes onlydecimal:delim,sheetandextstop instead of being accepted without effect.headings_frame()stops on an angle column held as text (character or factor), withradiatR_malformed_valuesnaming the values that are not numbers, orradiatR_invalid_argumentwhen all are. Withunits = "radians"and the unit-circle convention a character column used to load as a text heading column; otherwise these columns failed with bare base-R errors (“non-numeric argument to binary operator”, “‘abs’ not meaningful for factors”).A bare date whose midnight does not exist in
tz(a DST change at midnight, as in Cairo or Sao Paulo) is the first instant of that day (01:00 there) under every reader, and so is an Excel date cell at such a midnight. readr andfread()gave 01:00 for a column of such dates andread.table()an error; a date beside date-times stopped under readr. A date that does not exist at all intzis an error.Two date-times that used to load now stop with an error naming them, as bugs fixed: a leap second (
23:59:60, or235960in ISO 8601 basic) under readr, which rolled it over to the next minute, now stops with an error saying leap seconds cannot be represented, under every reader; and a date-time on a daytzskips altogether (2011-12-30 in Pacific/Apia) underdata.table::fread()andread.table(), which moved it a day back, now stops with an error saying it does not exist intz.derive_headings(rule = "straight")measures turns between moving steps only. A pause (a step of zero length) has no direction; it was read as a turn of 0, so a pause hid any turn around it, padded runs, and a run that began on a pause had heading 0 (East). Pauses now neither break a run nor count towardmin_len. Oncpunctatus, 47 of 235 trials have no straight run (20 before: 30 lost their run, 3 gained one), and 104 of the 185 with a heading either way change by more than a degree (median 90 degrees among those that change).derive_headings(rule = "pca_axis", source = "velocity")takes the axis of the steps about zero, not about their mean, so a track of equal steps has their direction as its axis (it had none, or one set by rounding). Under either source a track whose net displacement has no component along the axis (one that ends where it started) has headingNA: the axis has no sign, which had been left to the eigenvector routine. Oncpunctatus,source = "velocity"changes 198 of 235 headings by more than a degree (median 6.4, max 86.4);source = "position"changes none; no newNA.A built-in
derive_headings()rule stops with aradiatR_invalid_argumenterror naming any option in...it does not read (e.g.rule = "distal", frame_select = "all", which only the body-part rules read), and lists the options it takes. A misplaced option was silently ignored, and a longer misspelling was taken by partial matching (min_length = 50acted asmin_len); both now stop.return_coordsis accepted by every built-in rule. Registered and function rules still receive...unchecked.angle_convention, which no rule ever read (a heading is in the unit-circle convention), is among the options that now stop: drop it from such calls.The
ethovisiondialect now reads the text export EthoVision XT writes: a first line giving the number of header lines, a block of trial details, a names row and a units row,;-separated (or,or tab), in latin-1, UTF-8 or UTF-16, with-for a value the program did not have. Neither it noranymazeread a real export before. A file is one track (id"1"); its time isTrial time, in seconds; a row with the wrong number of fields stops the load, naming the line, so a file cut off mid-row has to have its partial last line removed. A table whose first line is its column header is read as a data frame, no longer as a file.ethovision:zone =is nowbodypart =(a zone is an area of the arena in EthoVision; the argument names body points). The default is the centre point;bodypart = c("nose", "tail")gives their mean.zone =stops with an error naming the new argument. The nose and tail columns are read by the names EthoVision documents (X nose,Y tail), which no export available to us contains.The
anymazedialect now reads ANY-maze’sTime,Centre position X,Centre position Yexport.Timeis a duration (0:01:02.500), read as seconds. The centre point alone is read:zone =is removed, as isskip_units_row =(the dialect deleted the first row whenever its position was not a number, which on a real export is the first sample). The columns it used to look for (X Centre,Nose X Centre) are in no export found. A file without a time column is an error, and so is aTimeof plain numbers, whose unit cannot be told: give a data frame with adifftimecolumn instead. Pass the file path, or readTimeas text (e.g.readr::read_csv(f, col_types = readr::cols(Time = "c"))): readr’s own time parsing drops the fraction of a second.Both dialects follow the rule for missing points (see “Masked points” in
?read_tracks): a sample without a position has anNAposition, is counted in@meta$attrition, and stops the load underon_invalid = "error"or is dropped under"drop". A malformed cell stops the load (radiatR_malformed_values); both dialects coerced silently before. Both declare their time as seconds. The mean of several EthoVision points divided the sum of y by the count of x present; it now masks a point missing either coordinate.draw_tracks(),line_circle_intercept_df(),line_circle_intercept_traj()andnew_headings_frame()are no longer exported. They are internal helpers: draw tracks withradiate()orgg_traj(), intersect a segment with the unit circle withline_circle_intercept(), and build a headings frame withheadings_frame().read_tracks()reads a delimited file the same way whichever reader is installed (see “Reading delimited files” in?read_tracks), and stops where the readers used to disagree. The fields of each line are counted first, and a row with more or fewer fields than the header is aradiatR_malformed_fileerror naming its line:fread()kept the rows before it,read.table()wrapped a long row past the fifth line into extra rows, and readr shifted the fields, with at most a warning; readr dropped a last line cut off mid-row without one. So is a line of spaces (readr skipped it;fread()stopped reading there), an unclosed quote, a table written with row names or a separator at the end of every row (one name fewer than fields), which each reader read differently, a first linefread()skipped as a preamble, and a column name used twice in a file read without a dialect (readr renamed both, so the file lost its id column and became one track). Every row of the file must be read. Underread.table()a#no longer cuts a cell short, an apostrophe no longer swallows rows and an empty cell is missing (a blank id was a track of its own);fread()reads past a blank line and keeps a text cell’s surrounding whitespace, as the other readers do. Column names lose their surrounding whitespace under every reader (readr kept" id", so no id was found). A gzip, bzip2 or xz file is read underfread()without R.utils (it stopped asking for R.utils, or read an xz file as text). A line break quoted in a column name or cell of a file with Windows (CRLF) line endings is read as"\n"under every reader, asread.table()read it (readr andfread()kept"\r\n", somapping = list(time = "fr\name")found no column).Ids that differ only by surrounding whitespace (
" 1"and"1") stopread_tracks()with aradiatR_ambiguous_idserror: readr andread.table()read them as two tracks andfread()as one.get_all_object_pos()reads its landmark and track files strictly: a cell that is not a number (a decimal comma, a word) is aradiatR_malformed_valueserror naming the file, row and column, where it failed withnon-numeric argument to binary operator; a line with fewer than three fields is aradiatR_malformed_fileerror (fields past the third are not read, and need not be on every line). An odd number of landmark rows is aradiatR_malformed_fileerror naming the file (it was a warning, then an error that named none). When no file holds a valid trial it stops withradiatR_no_valid_trialsinstead of returningNULLwith a warning.The
deeplabcutdialect now reads the predictions CSV DeepLabCut writes: header rowsscorer/bodyparts/coords, andindividualsfor multi-animal projects, which load as one track per individual (dialect_args = list(individuals = )selects some). The frame index is the time, in frames.deeplabcut_multiheaderis removed;read_tracks()namesdeeplabcutas its replacement (radiatR_removed_dialect). A flat<bodypart>_xtable is still accepted as a data frame, no longer as a file. Labelled training data (CollectedData_*.csv, indexed by image) is refused with an error saying so; the bundled example is nowdlc_single_wasp_predictions.csv, a predictions file.deeplabcutandsleapapply one rule to missing and low-confidence points (see “Masked points” in?read_tracks). A point is masked when its coordinates are missing, its score is 0, or it is belowlikelihood_min/score_min. A frame with a masked point has no position, so it stops the load underon_invalid = "error"and is dropped under"drop";dialect_args = list(partial = "mean")averages the points that remain instead. Before, a threshold on one bodypart removed rows without saying so, a threshold on several averaged whatever passed, and DeepLabCut’s (0, 0) placeholder for a frame with no detection was read as a position. The per-bodypart columns areNAwhere the point is masked.What those dialects masked is reported in one message (class
radiatR_attrition) and kept inTracksmetadata (@meta$attrition): per file and point, the rows masked, the rows read and the rows left without a position.c()row-binds the records of its inputs.A threshold now needs a score column for every selected point, and must be one number; a negative score is malformed (
radiatR_malformed_values). DeepLabCut and SLEAP columns are checked strictly, like the other dialects’: a threshold used to be compared as text when the score column was text ("1e-3" >= 0.9).bodypartmust name distinct points, and two SLEAP headers that differ only in case or punctuation are refused.DeepLabCut and SLEAP time is declared as frames (files, and data frames whose time column is a frame column), so
fpsalone gives elapsed seconds andtime_type = "seconds"is refused for them.derive_headings(carry = )now stops when a carried column has the name of a column of the result (id,time,heading, …). It used to replace the derived values with the carried ones without a message, so a call that relied on that now fails withradiatR_invalid_argument: rename the column in the tracks first.Tracksobjects now enforce the invariants the rest of the package assumes (see “Invariants” in?tracks): no missing ids; finite, strictly increasing time within each id; finite angles and coordinates; finite, non-negative weights. Previously a missing id, a non-finite or repeated time, an invalid weight, a non-finite relative coordinate (rel_x/rel_y), or any non-finite coordinate written after construction was accepted, and a missing time crashed the validator.tracks()now reports every such violation at once as aradiatR_invalid_trackserror whoseproblemsfield lists the rule, column, input row and id.read_tracks()passes that error on, after its own checks for non-finite x/y (radiatR_invalid_rows) and for duplicate keys across files. Consequences: relative-coordinate columns must be finite when the object is built, so give them finite placeholders (or name the roles without storing the columns) ifset_reference()is to fill them;radiate()on a data frame fails whengroup_coldoes not identify single trajectories (it used to draw one path per group that hopped between trials), so pass the trajectory id asgroup_col; and aTrackssaved by an earlier version that breaks a rule fails at its nextvalidObject()(any transform), not when it is loaded.load_tracks()andload_tracks2()have been removed. Despite the names they loaded no tracks; they joined manifest columns onto the file table used by the landmark pipeline. Pass the manifest toget_all_object_pos()instead, via its newmanifestandmanifest_colsarguments:get_all_object_pos(file_tbl = file_tbl, track_dir = track_dir, manifest = manifest).manifest_colsreplacesload_tracks2()’scolnames.The
cpunctatus_tracksdataset has been removed. It duplicated the table already stored incpunctatus; useas.data.frame(cpunctatus)(orcpunctatus@data) for the same per-frame rows.simulate_tracks(seed = )no longer changes the session’s random-number state: the simulation is drawn underseedand the previous state is restored on exit. Seeded output is unchanged. Code that relied on the call to seed later draws (a bootstrap or Monte-Carlo test run afterwards) must now callset.seed()itself.-
read_tracks()stops on a malformed value instead of treating it as missing. A cell in an x, y, angle, weight or time column that is present but does not parse ("oops","1,5", a stray header or units row) raises aradiatR_malformed_valueserror, a subclass ofradiatR_invalid_rows, that names the column, the data rows, the offending values and the file (each file’s own rows in a multi-file import).on_invalid = "drop"does not apply to it:"drop"removes only rows whose coordinates are missing or non-finite. Blank cells andNAare still missing values, andnanandinfare read as such. Previously one bad cell maderead_tracks()report every row of the column as non-finite, and"drop"then dropped them all; a bad cell after the first 1000 rows of a file was read as missing when readr is installed; and in a multi-file import the bad cell surfaced as a type-clash error from vctrs.Files are now read with column types guessed from every row and with no digit-grouping mark, so
1,5or1,234.5in a tab-separated file is malformed rather than read as 15 or 1234.5. Passread_opts = list(decimal = ",")for decimal commas. Where the decimal mark is a comma, one bad cell names that cell only, not every1,5in its column, and a value holding a.is malformed in turn, since1.234could as well be 1234. A semicolon-separated file is taken to use decimal commas unless its first lines show decimal points and no decimal comma: a value such as1.5or0.125, which cannot be digit grouping. For a fileread_tracks()reads itself,read_opts = list(decimal = )settles it either way.The check covers the columns
read_tracks()converts itself and the time column of every dialect but ANY-maze. An error raised inside a dialect does not yet name the file or the source column, and counts rows after the dialect’s own filtering and reshaping. Coordinates that a dialect converts itself (ANY-maze, EthoVision, DeepLabCut, SLEAP) are not yet checked this way. Text time columns are parsed strictly. Every value must match one timestamp layout in full:
YYYY-MM-DD HH:MM:SS,YYYY-MM-DD HH:MMorYYYY-MM-DD(with-or/), or ISO 8601YYYY-MM-DDTHH:MM:SSwith an optionalZfor UTC; a bare date beside date-times is midnight. An ISO 8601 timestamp, or one followed by stray text, used to be cut to midnight of its date without warning; the first now keeps its time of day and the second is an error. A text column of numbers is read as numbers (it was an error) and, withfps, stored as frames. A factor time column is read by its labels, not its level codes. ADatecolumn becomes midnight timestamps instead of day counts, and adifftimecolumn is converted to seconds whatever its units.time_type = "frames"with a date-time, date or duration column is now aradiatR_time_type_mismatcherror; it used to store epoch seconds as frame numbers.read_tracks()stops with aradiatR_missing_columnserror whenmappingnames a column that is not in the data. A missinganglecolumn used to be ignored without notice, and a missing x or y column gave an error that did not name it. Aregex_mappingpattern (seeread_tracks_format()) that matches no column is the same error, naming the role and the pattern.The
validateargument ofread_tracks()is deprecated. It never skipped anything, becauseTracksinvariants are checked whenever an object is built; any value butTRUEnow warns, and the argument will be removed.The
trackmatedialect stops with aradiatR_unlinked_rowserror (aradiatR_invalid_rows) when a file holds spots that belong to no track (TRACK_IDNoneor blank, as in an “all spots” export). They used to be pooled into one trajectory calledNone. Passdialect_args = list(unlinked = "drop")to leave them out, with aradiatR_dropped_rowswarning. Without a units row sayingPOSITION_Tis in seconds, the dialect now takesFRAMEas the time column rather thanPOSITION_T.Built-in loader dialects are now kept only for tracking software that is widely used and still developed. Eleven dialects are removed:
tracktor,boris_xy,dtrack,motchallenge,csv_bbox,wide_prefix_xy,tum_traj,euroc_mav,gpx,geojson_linestringandnmea_gprmc.read_tracks(dialect = )with one of these names stops with aradiatR_removed_dialecterror. To keep reading such files, register your own loader withregister_loader_dialect(); a loader registered under a removed name works as before. The bundledtracktor_example.csvis removed with its dialect, andxml2is no longer a suggested package.dtrack_read()is removed.vignette("loaders")shows a dtrack loader of a dozen lines, registered withregister_loader_dialect(), that reads the bundled millipede track files.A dialect is now the only reader of its input.
read_tracks(path, dialect = )used to parse the file with the generic reader first and discard the result, which printed repaired-name messages for headerless files and could fail before the dialect ran. With several paths the dialect is called once per file; a custom dialect that expected the whole vector of paths must be changed to take one path.read_tracks("<dialect name>"), which passed the name itself to the dialect as input, is no longer accepted.The raw example track files for
cpunctatusnow ship as one archive,inst/extdata/tracks.tar.gz, instead of the 470-file directoryinst/extdata/tracks/, which cuts about 2 MB from the installed size.system.file("extdata", "tracks", package = "radiatR")no longer finds anything; unpack the archive first withuntar(system.file("extdata", "tracks.tar.gz", package = "radiatR"), exdir = tempdir())and usefile.path(tempdir(), "tracks"). The files themselves are unchanged.derive_headings()rulesvelocity_mean,vm_fit,velocity_axisandorigin_meanleave out what has no direction: a step of zero length (a pause) and a position at the origin. Each was counted as pointing along the x axis (atan2(0, 0)is 0), which pulled the heading of any track with pauses towards it. Incpunctatusa quarter of the steps are pauses, and the defaultvelocity_meanheading moves by more than 1 degree in 194 of 235 tracks (median shift 12 degrees). A track that never moves now has headingNA; it had heading 0.The weighting options of those rules now do something.
weight_by(forvelocity_meanandvm_fit) andr_power(fororigin_mean) were passed tocircular::mean.circular(), which has no weights argument, so every value gave the unweighted mean.weight_by = "step_length"now weights each step by its length, which is the direction from the first position to the last, asnetgives;r_power = 1gives the bearing of the centroid.weight_bydefaults to"uniform"forvelocity_meanandvm_fit(the argument default was"step_length", which had no effect), so their headings remain the mean of the step directions.velocity_axiskeeps"step_length", which it already applied.vm_fitreturns the same heading asvelocity_mean: its von Mises fit raised an error that was caught, and the maximum-likelihood mean direction is the mean direction in any case.origin_meanstops withradiatR_invalid_argumentwhenr_poweris not one finite number; such a value was read as 0.The other
derive_headings()rules no longer read a displacement of zero length as pointing along the x axis.net,straight,goal_bias,maxspeed_windowandwindow_netgiveNAfor a track that never moves (they gave 0),pca_axisgivesNAwhen the positions, or the steps undersource = "velocity", have no spread (it gave pi/2), anddistalgivesNAfor a track that stays at the origin.netalso givesNAfor a track that ends where it started, andstraightfor a straight run made of pauses only (3 of 235 tracks incpunctatus). All are reported byon_missing.window_netgivesNAfor a window whose first and last positions are the same (350 of 39631 windows incpunctatus, which had heading 0), soon_missingnow warns about them.get_tracked_object_pos()andget_all_object_pos()withradius_criterion = "first_past"give a landmark (x0/y0,x1/y1inmeta$trial_limits) ofNA, with a warning naming the trial, when no point of the trial reaches that radius. The farthest point was substituted, with a warning only for the inner radius. A trial that is not mapped (it starts too far from the centre) hasNAlandmarks andorder/vid_ordintrial_limits, not 0.get_all_object_pos()keeps thetrial_limitsrows of a file none of whose trials is valid. The bundledcpunctatusfollows suit: trialcon_19_1, which never reaches the inner radius, now hasNAlandmarks inmeta$trial_limitsinstead of its farthest point (0.059, 0.015) for both. Nothing else in the dataset changes.readr no longer decides how a delimited file’s date-times are read: like
data.table::fread(), it hands them back as the text in the file, and one parser reads them for every reader. That parser now also reads ISO 8601 basic and hour-minute times, zoned or not (20240101T100000,20240101T100000Z,2024-01-01T1000+02:00,2024-01-01T10:00). A time column that mixes zoned (Z,+02:00) and unzoned date-times, or holds hours alone (2024-01-01 10), a trailing decimal point, seconds on some values only or a basic date and time split by a space (20240101 100000) now stops under readr too, with an error naming the values; so does an offset after a space (2024-01-01 10:00:00 +0200), whichfread()read. A kept column (keep =) whose every value is in one date-time layout (or is a date) is read as date-times intzunder every reader, wherefread()andread.table()gave its text; any other kept column is kept as read, the same under every reader (readr typed some, such as hours alone). So a kept column can now stop the load underfread()andread.table(), where it stayed text: one holding a time that does not exist intz(a DST gap), a leap second, or zoned and unzoned date-times in one layout. Files whose unused date-time columns were typed differently (a CSV beside an xlsx) bind again. A time column of date-times read withtime_type = "seconds"or"frames"now stops withradiatR_malformed_valuesunder every reader, as it did underread.table(); readr gave epoch seconds (or, for frames, aradiatR_time_type_mismatcherror).A file given to the wrong built-in dialect now stops instead of loading wrong positions or ids. The
sleapdialect needs aframe_idxcolumn unlesstime_colnames the time: it read any<name>_x/<name>_ypair as a node, so a ToxTrac file loaded its bounding box and a TRex file its midline. Thetrexdialect refuses a track column (Track,TrackID,TRACK_ID,trajectory_id) unlessid_colnames the id: it ignored one, so a ToxTrac file’s tracks became one id and a TrackMate file’s spots one track each.id_colandtime_coloftrexandtrackmateare now matched as the headers are ("Track"failed with an untyped error) and a name that matches no column stops withradiatR_dialect_error, as do the other refusals oftrex,trackmate,toxtracandidtrackerai_wide, which were plain errors. These refusals,sleap’s, and a misnamedbodypart,id_colortime_colin any built-in dialect name the file, which a multi-file read did not say.sleapmatchesid_col/time_colas it matches its headers, too.
Changes
The app has a “Decimal mark” control (Auto-detect / Point / Comma) beside Delimiter for text uploads. It applies to previews, trajectory and headings loads and the downloaded scripts, and an error about values written with the other mark says which setting to choose. A tab or comma file with decimal commas, and a
;file whose decimals look like digit grouping, can now be loaded in the app.The
sleapdialect accepts a node name as it is written in the file (bodypart = "Tail Base").A dialect can keep facts about the file it read with
attr(out, "source_meta")(see?register_loader_dialect).read_tracks()stores one record per file inTracks@meta$source_meta, with the file’s name and path;c()concatenates them.ethovisionkeeps the export’s header block (header: trial, arena and subject names, start time, …) and the units of its columns (units) there.The message about masked points offers
partial = "mean"only when the position was taken from more than one point.The app detects EthoVision and ANY-maze uploads.
The app detects DeepLabCut files by their header rows, multi-animal files included. A flat
<bodypart>_likelihoodtable is still offered to thedeeplabcutdialect, which now refuses it by name; choose “Generic CSV” to load it as plain columns. A dialect’s refusal of a file is shown as the app’s error headline.The app can load a file that has rows with no position. When a load stops on rows with a missing x/y, or on TrackMate spots that belong to no track, the error offers to drop those rows and continue. Nothing is dropped unless the user agrees, and the app asks again on every later load. The number of rows dropped is shown above the results, and the downloaded scripts load the file the same way (
on_invalid = "drop",dialect_args = list(unlinked = "drop")). This lets idtracker.ai files, TrackMate “all spots” exports and DeepLabCut or SLEAP files with masked frames load in the app, which refused them before. Malformed values and duplicate id/time rows are still errors.New vignette
vignette("agent-guide"): the angle conventions, calibration rules, failure behaviour and common pitfalls an AI agent (or anyone scripting radiatR) needs.AGENTS.mdat the repository root points to it. Thecirc_summarise()help now states thatmean_dir_degis in the display convention rather than a plain conversion ofmean_dir, and theread_tracks()help thatkeepadds to the mapped columns (it saidNULLkeeps every column; unmapped columns are dropped).The main workflow and simulation walkthroughs are now website-only articles (
vignettes/articles/) rather than installed vignettes, sovignette("radiatR")andvignette("simulate")are no longer available offline; read them at https://johnkirwan.github.io/radiatR/articles/. Together with the dataset removal this brings the source tarball under CRAN’s 5 MB limit, and R CMD check now fails if it grows past 4.5 MB.The hosted Shiny app has moved from shinyapps.io to Posit Connect Cloud (git-backed deploy). The
shinyapps.yamlGitHub Actions workflow is removed; Connect Cloud rebuildsinst/appfrom the repo and installs dependencies from the committedinst/app/manifest.json, regenerated by the newconnect-manifestworkflow.Reading several track files at once in
read_tracks()no longer needs the optional dplyr package (it is bound withvctrs::vec_rbind()). purrr is no longer a dependency; vctrs, already required by tibble and ggplot2, is now imported directly.radiate()draws clipped tracks much faster: clipping paths to the unit circle (clip_tracks = TRUE, the default) is now vectorised, taking a plot of thecpunctatusexample data from about 6 s to about 0.1 s. The drawn geometry is unchanged.line_circle_intercept()is now vectorised over its arguments.Nineteen help pages in the circular-plotting and circular-boxplot groups now have runnable examples:
circ_display(),directedness_arrow(),compute_circ_interval(),add_circ_interval(),add_heading_interval(),compute_circ_mean(),add_circ_mean(),add_heading_arrow(),add_stacked_headings(),line_circle_intercept(),gg_traj(),add_angle_rose(),add_vonmises_density(),add_circular_kde(),add_wrappedcauchy_density(),add_critical_r(),add_critical_v_line(),circ_boxplot_stats()andadd_circular_boxplot().
Bug fixes
read_tracks(<data frame>, read_opts = list(decimal = ","))reads a column of “1,5” text as 1.5; the option was ignored, and the error about such values now suggests it for a data frame too.App: a headings file read with the wrong decimal mark no longer has its id column plotted as the angles. The angle picker only listed numeric columns, so an angle column read as text was missing and the first numeric column (often the id) was preselected. A text column of numbers in the other mark is now listed and preselected, with a note saying which Decimal mark to set, and Continue stops with that advice.
App: the script downloaded for a headings upload reads the file with the delimiter and decimal mark the app read it with. With the delimiter control on “auto” (the default) a
;file was read withread.csv(), which stopped, and a;file with decimal points was read withread.csv2(), which left its numbers as text. A latin-1 file is read withfileEncoding = "latin1".read_tracks()reads a zip archive without readr or data.table installed. Theread.table()fallback was handed the archive itself and stopped with a misleading “0 row(s) were read” error; it now reads the archive’s first member, as readr andfread()do, a latin-1 one included.launch_app()no longer writes to the global environment. The app sourced its helper files there (functions and constants such asbuild_plot_spec()andAPP_SEED), which CRAN policy forbids; they now live in the app’s own environment.A latin-1 delimited file (as Windows programs write) reads as its text under every reader: a byte such as
µin a column name oréin an id stopped the load with an untyped “invalid UTF-8” error, and one in another text column was kept as an invalid string. As for a tracker’s dialect file, one line that is not valid UTF-8 makes the whole file latin-1, so in a file that mixes the two, its UTF-8 characters read as two latin-1 ones each (µasµ).read_tracks(dialect = "deeplabcut"): a latin-1 body-part name (a header re-saved by a Windows program) reads as its text. It made the file “not a DeepLabCut output file”.App: a headings upload’s group columns, and the generic preview’s mapped id column, keep their text where reading them as numbers would merge labels (
1.1and1.10), asread_tracks()already did for loads. The downloaded script names such group columns incolClassesso they are read as text too.App: a file read by a tracker’s dialect that the generic preview cannot make a table of (a raw EthoVision export) is previewed as its first lines. It showed one column of preamble text under readr and could not be previewed under
data.table::fread().A time that does not exist in
tz(the hour skipped when clocks go forward) stops with an error saying so under every reader; underfread()andread.table()it said only “not a date-time”. (A skipped day and a leap second: see Breaking changes.) A column of zoned and unzoned date-times in one layout stops with an error saying the zones are mixed, not that a valid value is no date-time. A bare date on a daytzskips reports its own row. An Excel date cell on a daytzskips altogether stops the load only when its column is used or kept, and reports its row.A multi-file import whose time column holds bare dates in one file and date-times in another (a CSV of dates beside a CSV or an xlsx of date-times) binds under every reader, each date the first instant of its day in
tz.fread()stopped with a type clash, and under readr a date on a daytzskips was reported as a missing time.derive_headings()no longer calls headings “trials” for a rule that gives several per trial (window_net,crossing): theon_missingmessage counts missing headings and the trials they come from, and the app’s attrition banner does the same.missing_idslists each trial once, and the newn_trialsattribute gives the number of trials.read_tracks(id_collision = "namespace")gave files that share a name less extension (n1.csvandn1.xlsx, ora/n1.csvandb/n1.csv) the same prefix, so their ids still collided. Such files are now prefixed with their full name, or the end of their path that tells them apart (a/n1.csv).Ids written with surrounding spaces and no unpadded twin (
1,10) are the same ids under every reader: readr kept them as the text" 1", wheredata.table::fread()andread.table()read the number 1.The separator sniff of
read_tracks()counts fields as the readers do, so a separator inside a quoted field no longer counts: a;file with;in a quoted label is read as;. A file whose header alone has a different number of fields (a table written with row names) is named as such; it was read as one column. A delimited file that still reads as a single column stops with aradiatR_malformed_fileerror saying no separator was found, instead of “Need either an angle column or both x and y columns”.read_tracks()no longer stops with “invalid UTF-8” in the separator sniff when the first lines of a delimited file hold a byte that is not UTF-8 in a data cell that is not an id (a latin-1 label): the separator and decimal mark are sniffed from such lines too. A latin-1 byte in a column name or in an id column still stops the read.read_tracks()with atzother than"UTC"put a timestamp that names its zone (2024-01-01T10:00:00Z,...+02:00) at the wrong instant when the file was read bydata.table::fread()(readr not installed): the instant was written back as UTC wall-clock text and read again intz, moving it by the zone’s offset (10:00Z read in New York as 15:00Z), and its fraction of a second was dropped. Such columns are now read again as the text in the file. Text time also reads a UTC offset (+02:00,-0130,+02, at most 14 hours), so a file with offsets gives the same instants under every reader;read.table()refused them before. AZor offset is read after any accepted layout (2024-01-01 10:00:00Ztoo). On the text path a column that mixes zoned and unzoned date-times is malformed, as one mixingZwith unmarked times was.gg_traj()withfacets =stopped when the plot was drawn: it askedfacet_wrap()for free scales, whichcoord_equal()does not allow. The panels now share fixed scales, so trajectories on the unit circle are drawn at the same scale in every panel.Ids are kept as labels in more cases (see “Ids” in
?read_tracks). Two files of one import whose ids read as one number, written differently (1.1in one file and1.10in the other), are two ids: they were reported as colliding, andid_collision = "namespace"labelled both1.1. An id column typed as a date or time keeps its text where two labels are one instant (2024-01-01 00:00and2024-01-01 00:00:00were one track). An Excel file’s ids are compared with a delimited file’s as the text of their cells (100000), andfread()’s large whole numbers (integer64) as text too. Where the id types still disagree, in tables that cannot be read again for their text (two tables from one dialect, a Parquet file), the load stops withradiatR_id_type_mismatch; it stopped with an internalCan't combineerror.derive_headings()andcirc_summary_headings()now requirerule. Called without it,derive_headings()stopped with an internal'length = 16' in coercion to 'logical(1)'error, andcirc_summary_headings()fell back to"crossing", which then stopped for want ofcirc0andcirc1. Both now stop with an error that lists the available rules.derive_headings()also refuses arulethat is not a single name or a function (a vector,NA, a number), which previously gave the same internal error.read_tracks(tz = )now applies to every timestamp without a time zone in a delimited or Excel file thatread_tracks()reads itself. A timestamp the file reader typed (YYYY-MM-DD HH:MM:SSwith readr or data.table installed, or an Excel date-time cell) was read as UTC whatevertzwas, while one left as text (YYYY/MM/DD HH:MM:SS) was read intz: the same wall-clock time came out hours apart, between two files of one import or between two machines with different readers installed. A timestamp markedZis still UTC, and a bare date beside such timestamps is midnight intz. A wall-clock time that does not exist intz(the hour skipped when clocks go forward) stops the load instead of being moved an hour on.tzdoes not yet reach a file read by a dialect, or a Parquet or Feather file.read_tracks(format = )honours the format. It passed all of its own defaults on toread_tracks_format(), where they overrode the format: the format’smappingandregex_mappingwere discarded and the columns guessed from their names (a format mapping x topos_xread a column namedxif the file had one), and its angle unit, time type, time zone and frame rate gave way to the defaults, so degrees were read as radians. Now only the arguments the caller supplies override the format,mappingrole by role, andread_optsis passed on. An argument given asNULLcounts as not supplied (fps = NULLorkeep = NULLused to delete the format’s frame rate or kept columns), adialectthe caller names replaces the format’s, andload_manifest(format = )follows the same rule: it passed on all of its own defaults, so a degrees format was read as radians there too. A format’sdialect_argsreach its dialect: they were handed toread_tracks()as arguments of its own, which stopped with “unused argument”.Ids that are written differently are no longer merged into one track. A file reader types an id column as numbers when it can, so in any delimited file ids
1.1and1.10,1and1.0,1e1and10(and01and1without readr) were read as the same id, with no condition.read_tracks()now keeps such a column as the text in the file (TandTRUE, read as one logical value, likewise). The same holds for the mapped id column and for any column namedid,track,trajectory,animalorsubject. Ids that read as distinct numbers are numbers as before. Where the delimited files of one import disagree on the type of the id column, the ids of each file are the text in that file; the import used to stop with a type error, or readTRUEbeside numeric ids as 1.A time that does not exist in
tz(the hour skipped when clocks go forward), in an Excel date-time cell or in a delimited file read with readr, stops the load with aradiatR_malformed_valueserror naming the file, column, row and value, when it is in a columnread_tracks()uses: the time column or one kept withkeep =. It was read asNA, which blanked a kept column (without a word from an Excel file, with readr’s “parsing issues” warning from a delimited one) and gave a time column an error that named neither the value nor the cause. In a column that is not used it is stillNA.circ_summarise()andcirc_summary()on aTracksobject now return the von Mises concentrationkappa; it was alwaysNA. The estimator was looked up ascircular::est.kappa, a function the circular package does not have, so every caller silently got its fallback instead.kappais now computed directly as the maximum-likelihood estimate from the mean resultant length (the valuecircular::mle.vonmises()gives), weighted where weights are in use. Both reportNAfor fewer than three valid angles (ascirc_summarise()already did) andInfwhen all angles coincide, where the estimate is unbounded.circ_summary_headings(),boot_kappa_ci()and the starting value inconcentration_regression()already used this estimate and are unchanged.-
The
trackmate,toxtrac,idtrackerai_wideandtrexdialects now read the files those programs write. Checked against real exports:- TrackMate 7 spot tables load; their three extra header rows used to be reported as malformed values. They are dropped only when recognised as names and units rows.
- ToxTrac
Tracking_RealSpace.txt(Time (sec),Track,Pos. X (mm)) and the headerlessTracking.txt(6 or 11 columns) load; both used to stop with “required columns not found”. The id is<arena>_<track>. - idtracker.ai
trajectories.csv(seconds,x1,y1,...) loads; it used to stop with “no x_/y_ pairs found”. An x column with no y column is now an error, as is an idsentry the file does not have. - TRex files that write
frametwice use it as the time; they used to fall back to thetimecolumn. These dialects check their source columns strictly, so a malformed value is reported by the header as written, the row of the file and the file name.
Dialects now state the unit of their time column, so seconds are no longer taken for frames when an
fpsis supplied (elapsed time and speeds were divided by it). See “Time units from a dialect” in?read_tracks. Anfpsgiven for a time column in seconds is not used, with a message; an explicittime_typethat contradicts the dialect is aradiatR_time_type_mismatcherror.App: tracking software is now recognised from the file’s header for TrackMate, ToxTrac and idtracker.ai exports. The header was reduced before it was lower-cased, so no header containing a capital letter ever matched. A file whose time is in seconds no longer needs a frame rate for the kinematics.
Numeric time loaded as seconds is now treated as seconds. A
Tracksread withread_tracks(time_type = "seconds"), or from a duration (difftime) column, was handled as a frame index:elapsed_seconds(),track_speed()and the other kinematics stopped and asked for a frame rate, and once one was set they divided the seconds by it. Such aTracksnow recordstime_unit = "seconds"in its metadata and needs no frame rate; a frame rate set on it is kept but not applied.c()refuses to combineTrackswhose time units differ. Numeric time of unstated unit is still a frame index, and the error that asks for a frame rate now mentionstime_type = "seconds".read_tracks()andread_tracks_dir()now work withdialectand several files. They used to fail withis.data.frame(df) is not TRUE. Each file is read by the dialect and coerced on its own, so a malformed value is reported against its own file and row; ids repeated across files followid_collision, and a dialect that returns no id column gets one id per file. A failure inside the dialect is re-raised as aradiatR_dialect_errornaming the file, unless it is already a radiatR condition.read_tracks()names the file(s) that do not exist instead of reporting thatxmust be a data frame or path.read_tracks()ignores a guessed id column that holds no values. It uses the next id-like column if there is one, and otherwise reads the file as one trajectory, as it does when there is no id column. SLEAP writes an emptytrackcolumn for single-animal exports, sosleap_example.csvnow loads withoutdialect = "sleap". An id column named inmapping, or one that is only partly empty, is still an error.x, y, angle and weight columns stored as text or as a factor load when every value is a number. Previously every row was reported as non-finite.
The app’s error banner names the column and an example value when an upload has a malformed cell, instead of the generic “not valid track data”.
read_tracks(dialect = "sleap")now reads an export whosetrackcolumn is empty (SLEAP’s output for untracked, single-animal data) as one trajectory with id"1". Previously such a file failed to load, or with a single row loaded with a missing id.An angle within floating-point error below zero (for example
-1e-17, or a position whoseyis float noise below 0) no longer makestracks()fail. It wrapped to exactly2*pi, outside[0, 2*pi); it now wraps to 0.The script downloaded from the Shiny app now derives headings in the frame the app used. In the app’s default relative frame the script omitted
coords, soderive_headings()fell back to its"absolute"default and the script’s headings (and every statistic computed from them) differed from those shown in the app. The frame is now always written explicitly, as the one the app resolved: a dataset with no relative coordinates getscoords = "absolute".The Shiny app’s method preview now follows the “Heading frame” control. It always illustrated the relative frame, so with the frame set to absolute (or with data that has no landmark frame) the preview showed a different frame from the one the Results derive headings in.
The Shiny app no longer calls
set.seed()on the shared R process when it computes Monte-Carlo uniformity p-values, so one session can no longer shift another’s random draws. The mean-direction bootstrap CI arc and the RANSAC heading rule (in both the method preview and the results) are now drawn under the same fixed seed, so they stay put across redraws, and the downloaded script sets that seed to reproduce them.circ_summary_headings()with the defaultgroup_by = "id"no longer errors (“numbers of columns of arguments do not match”) when any trial produced no heading; such trials are returned withn = 0andNAstatistics.get_trial_limits()(and soget_all_object_pos()) no longer returnsNAmanifest metadata for the second and later trials of a video when the file table has fewer rows than the video has trials.circ_summary()on weightedTracksnow weightsmean_dir. It was the unweighted mean beside a weightedresultant_Randkappa, becausecircular::mean.circular()ignores aweightsargument. Results change for anyTrackswith a weight column, or withw =.kappaisNAunless three angles have a positive weight; zero-weight angles counted before.circ_summary()stops with an error of classradiatR_nonestimable, naming the id, when a group’s weights are all zero. It returnedresultant_R = NaNwithout comment.circ_summary(w = )checks the column it is given: a name that is not a column, or a column with missing, non-finite or negative values, is an error of classradiatR_invalid_argument. A misspelt name was ignored and the summary computed unweighted; rows with a missing weight were dropped.circ_summarise(display = )requires acirc_display()object and says so (classradiatR_invalid_argument).display = NULLfailed with “argument is of length zero”.
radiatR 0.1.2
Breaking changes
- The
ctraxloader dialect and its optionalR.matlabdependency have been removed..matfiles are no longer supported;read_tracks(dialect = "ctrax")now errors as an unknown dialect. Pre-computed orientation angles remain supported through the generalellipse_axisheading rule on any column. -
read_tracks()now fails on non-finite coordinate rows by default, reporting the affected row numbers and ids; passon_invalid = "drop"to drop them with a warning instead of the previous silent drop (breaking).
Bug fixes
- The Shiny app no longer shows a previous analysis’s results after a new headings upload. In headings mode a failed file upload used to leave the earlier (e.g. example) headings in place, and the “example headings already loaded” shortcut could then display them as if they belonged to the file just selected. The upload now clears any prior analysis before reading and commits the parsed data only on success, and the shortcut to Results fires only for genuinely example-sourced headings.
-
c(Tracks, ...)now rejects trajectory ids that collide across the objects being combined, and errors when the inputs carry conflictingframe_rate,distance_scale,distance_unit, ornormalize_xymetadata (including one input having it set and another not). Previously these were silently merged, discarding one object’s calibration in favour of the other’s, and could interleave rows from two different trajectories that happened to share an id. Per-id/per-run metadata (reference,trial_limits,sim_conditions,oob_points,oob_trials) is now merged/summed across all combined inputs instead of only the first object’s being kept. -
read_tracks()andread_tracks_dir()now preserve trajectory identity when importing multiple files. Previously, reading a character vector of paths with no id column (the common case, relying onid_from_filename = TRUE) silently collapsed every file into a single"(multiple)"trajectory whose rows were then interleaved by time across files. Each file now yields its own trajectory keyed by the file-name stem. A newid_collision = c("error", "namespace")argument controls what happens when a real id value appears under more than one file:"error"(the default) stops with a message naming the colliding id and files, and"namespace"prefixes every id with its file stem (file::id). Multi-file imports with no id column andid_from_filename = FALSEnow fail with a clear error instead of the silent collapse, and duplicate(id, time)keys produced by combining files are reported rather than passed through. -
read_tracks(..., time_type = "frames", fps = )no longer converts frame indices to seconds twice. The time column is now kept as a raw frame index andelapsed_seconds()(and everything built on it:track_duration(),track_speed(), velocity, and turning-rate) performs the frame -> seconds division exactly once, on demand, using the storedframe_rate(). Previously the loader divided byfpsimmediately and then stored the samefpsas the frame rate, so downstream kinematics divided byfpsa second time – e.g. an expected 60 units/s speed came out as 3600 units/s. Loading withtime_type = "seconds"no longer stores a spurious frame rate either. -
wrappedcauchy_fit()’sconvergencecolumn now matches its documentation.circular::mle.wrappedcauchy()reports convergence as a logical, which the previous code passed throughas.integer(), yielding1for a converged fit – the opposite of the documented “0= converged”. The column is now0when the fit converged and1when it did not, and the (incorrect) “optim return code” wording in the docs has been corrected. - Between-group tests (
test_mean_directions(),test_concentration(),test_distributions()) now signal a structuredradiatR_nonestimablecondition (carrying method, group, n, and reason) for non-estimable inputs, so callers can distinguish expected non-estimable cases from bugs.
App
- The app no longer assumes a capture rate of 30 fps. The frame-rate controls now start unset, and time/speed colouring, the kinematics plot and its downloads, and the generated kinematics code stay disabled until a positive finite frame rate is entered or carried by the loaded data. The app shows whether the active rate came from the data or was entered by the user, and downloaded kinematics code no longer emits a fabricated
set_frame_rate(ts, 30)when no rate is set. Previously, uncalibrated numeric-frame data produced plausible-looking speed and turning-rate results based on the unsupported default. - Ingest failures now show the original exception (with affected rows/ids) under “Technical details” and no longer suppress ingest warnings; the group-comparison table shows the non-estimable reason instead of a blank dash and re-raises unexpected errors; downloads are gated (
results_ok()) so a failed or partial run cannot emit an apparently-valid result.
radiatR 0.1.1
New features
- New
test_gof()tests whether a sample of headings fits a wrapped Cauchy distribution (Watson’s U^2 on the probability-integral transform, with a parametric-bootstrap p-value), closing the wrapped-Cauchy half of the non-von-Mises goodness-of-fit gap. Jones-Pewsey GOF remains deferred. -
test_uniformity()gainstest = "pycke", the Pycke (2010) omnibus uniformity test — like Hermans-Rasson, powerful against multimodal and asymmetric departures from uniformity, with a Monte-Carlo p-value.
Bug fixes
-
radiate()no longer labels every trajectory by its id when a plot has many tracks. The automatic fallback to the id/group column now applies only when that column has at most 12 distinct values, so many-track plots (e.g. the 235-trialcpunctatus) render cleanly instead of being buried under overlapping trial-id labels. Passlabel_colexplicitly to force labelling regardless of track count. -
radiate(clip_tracks = TRUE)(the default) now also clips beyond-circumference overshoot in the absolute frame (coords = "absolute"), not just the relative frame. The relative frame is a rotation of the absolute one, sorhois identical in both; the previous relative-only restriction letcoords = "absolute"plots (e.g. in the package vignette) draw tracks visibly outside the unit circle. -
get_all_object_pos()now reports out-of-bounds track points (radius > 1) with a single aggregated message across the whole manifest, instead of one message per file.
App
- Summary & stats now has a “Group comparison” card:
test_mean_directions,test_concentration, andtest_distributionsrun automatically across the selected Group-by column, showing whether groups differ in mean direction, concentration, or overall distribution (not just whether each is individually non-uniform). - The app now exposes an explicit Coordinates control for the per-trajectory shape transform (
normalize_xy), which was previously pinned on with no way to opt out. The transform fits each trajectory to the unit circle independently and preserves path shape, not bearings from a fixed arena origin; the control labels it as such rather than as a calibration. Downloaded figure, statistics, and kinematics scripts now emit thenormalize_xyvalue the app actually loaded with, instead of silently falling back to the package default.
radiatR 0.1.0
- First public release.
- Read and construct movement
Tracks, with support for absolute and relative coordinate frames and frame-rate / time metadata. - Plot circular data with
radiate(), including axial overlays, faceting, grouping, perimeter labelling and circular boxplots. - Compute kinematics: speed, path sinuosity, per-row velocity angle and related trajectory summaries.
- Circular statistics: circular and concentration regression, model selection, and tests of mean direction, symmetry and unimodality.
- Interactive Shiny app (
inst/app), also deployed at https://johnkirwan.shinyapps.io/radiatR/.
