library(platypus) is library(rplatypus). The name collided with Platypus,
archived from CRAN on 2026-02-02 - unrelated work in immunology - and the two differed
only in case, which the policy treats as a conflict and which archiving does not release.
cran-comments.md had promised to resubmit under another name rather than ask for an
exception, so this is that name rather than a new idea: one name per language, beside the
pyplatypus engine on PyPI, and a name that says what the package is - the R surface over
a Python engine.
rplatypus is in neither CRAN's current index nor its archive, checked against both with
a positive control so that the absence means something.
The functions did not change. platypus_spec(), platypus_fit(), platypus_status(),
platypus_device() and platypus_use_torch() keep their names, and so do the S3 classes
platypus_fit, platypus_spec, platypus_split and the rest, the platypus_* condition
classes, the PLATYPUS_* environment variables and the headers print() writes. The rule
is that the package's identity - what library() and CRAN see - is rplatypus, and the
brand is still platypus. §3A already said the S3 class keeps the prefix even where its
constructor loses it; this is the same line drawn one level up, and it means no released
surface moved.
The repository was renamed too, to maju116/rplatypus. Stars, forks, watchers and
every issue came with it - read back from the API rather than assumed - and
github.com/maju116/platypus redirects, so links in other people's issue comments keep
working. The site moved with the repository and is now at
maju116.github.io/rplatypus; the former address has
no redirect, because a project page is served under the repository's own name and the only
way to hold the old one would be to occupy the name the redirect needs.
The dated entries below name maju116/platypus where they were written that way. They are
the record of what it was called at the time and they still resolve, so they are left alone
rather than rewritten.
tools/reference-groups.yml into a
sidebar nested in eight groups, and themed from a _brand.yml that is byte-identical to
pyplatypus's - so the two sites look alike by construction rather than by anybody
remembering to copy a colour.This file has a gap and it is named rather than backfilled. The entry below is dated
2026-10-08 and the pin moved twice between then and here - to 0.8.0a12 for fives-unet
(#151) and to 0.8.0a20 for the instance task (#155) - so the dates above this heading
are not a complete record of what the pin did. Writing those entries now would be writing
them from the commit log rather than from the work, which is a different thing from having
written them; the heading says what this change is and the omission stays visible.
Engine pinned to 0.8.0a22, for two things R forwards rather than implements.
A regression loss is refused as any segmentation model's own loss, not only an instance
model's. ?losses claimed the narrower rule and was measured when it was written, so it
was true rather than wrong - the engine's refusal lived on the instance class alone, and a
semantic model accepted loss_mse() and trained with it, reporting a validation loss of
0.0014 for a mask that was half missing where loss_cce() reaches a Dice of 1.0000. The
default where = "foreground" is what made that look like a success: it averages the
objective only inside the labelled objects and leaves the background unconstrained.
pyplatypus#223.
So available_losses() is ten rows now rather than thirteen, and
available_losses("instance_segmentation") is thirteen - the three regression losses are
reachable where they have a target, which is separation_flows(loss = ). Nothing R sends
changes: a configuration refused after this pin move was refused by the engine before it.
evaluate() on an instance fit carries foreground_dice. It is the mask column to
read across models - the binary overlap of anything the model called an object against
anything that is one, the same arithmetic whatever the separation is. The dice beside it
is a mean over the class channels the head has, three for separation_rim() against two
for separation_flows(), so it is read down one model's rows and never across two: on the
Data Science Bowl's validation cases it showed two masks 0.09 apart that are 0.0062 apart,
and in the other direction. pyplatypus#222.
?losses' task parameter listed two tasks and there have been three since the engine's
0.8.0a20.metric_cldice(), for structures where Dice asks the wrong question. It scores each
mask against the other's skeleton, so it measures whether a vessel is connected rather
than whether its pixels coincide. Vessels, airways, neurons, cracks, catheters.
The two disagree in a way worth knowing, measured on a nine-pixel vessel: one drawn two pixels thin scores Dice 0.875, and the same vessel severed in the middle scores 0.947 - Dice prefers the severed one. clDice gives them 1.000 and 0.943. Its limit is measured too: at one pixel wide, a one-pixel displacement leaves no overlap at all and every overlap-based metric reads 0, this one included.
include_background defaults to FALSE here and TRUE for the other three. The
background's skeleton lies inside the background by construction, so that class scores 1
whatever the model did.
0.8.0a2.drawing_style(), the five decisions every figure in this package makes when you do
not give it one: which colours mean found, missed and invented, how much of the image an
overlay lets through, how a box is labelled, and which boxes are worth drawing. Reach for
it when you want to match a figure rather than restate it -
colours = drawing_style()$box_colours keeps one source where typing the hex codes
starts a second.The drawing decisions now come from the engine. They were written out in ten places
across R/plot.R and R/masks.R, and pyplatypus.style held the same five values -
which is two copies that happened to agree, not one source. They agreed because five
differences between the two packages' figures had been found by laying them side by side
and fixing them by hand.
Nothing changes in any picture: the values are identical, and a test compares every one of them against the engine's, so a change on that side now fails this package's CI instead of being noticed by eye.
Drawing still needs no Python. That is why the values are mirrored in R/style.R
rather than fetched: plot_masks(), plot_boxes() and overlay_agreement() return
without touching the bridge, and a default colour that called the engine would mean a
five-gigabyte download to draw a mask.
Eight functions were renamed. The engine and this package had grown two words for the
same thing in eight places, and a reader moving between the halves paid for every one.
One name per concept now, and where the two disagreed the engine's name won, because
it is the layer that cannot be renamed later without changing the configuration format
and the schema. The exception is available_transforms(), where this package already had
a family of available_* names and the engine followed instead.
| was | is | why |
|---|---|---|
| platypus_split() | split_dataset() | the engine's name. The S3 class is still platypus_split - that is the object's identity, and every class here is prefixed |
| split_files() | split_path() | it reads one split's path; it does not split files. split_samples() in the engine is a different function, and the two names invited pairing them |
| mask_report() | inspect_masks() | the engine's name, and the method it calls |
| mask_classes() | colours_to_classes() | the engine's name, and it says which way the conversion goes |
| mask_colours() | classes_to_colours() | likewise. Note the engine's onehot_to_colours() is not this function - it takes probabilities and argmaxes them, this takes class indices |
| save_weights() | export_weights() | the engine's name; "export" says a sidecar is written beside the file |
| available_augmentations() | available_transforms() | the engine's name, and albumentations' own word for them |
| augment() | augmentation_step() | augment is a generic in broom and means something else there. augmentation alone would have shadowed the augmentation = argument it builds a list for |
No aliases: two names for one thing is what this removes. The S3 class returned by
split_dataset() is still platypus_split - that is the object's identity, and every
class in the package is prefixed.
evaluate(), evaluate_cases(), evaluate_classes() and evaluate_images() keep their names
and so does platypus_fit(). The prefix on that one is not an inconsistency: fit is a generic
in the tidymodels packages, so a bare fit would have been theirs to own, while evaluate is a
generic here and dispatches on the fit's class whichever package was attached last.
PYPLATYPUS_VERSION is pyplatypus_version. It was the only ALL-CAPS name in the
package, so keeping it would have meant a lint exception for a style used exactly once.
callback_swa(): average the weights over the last part of the run. What it is worth
is not in the score column - Dice and per-case error do not move - it is in the spread.
Measured over three seeds, the volume bias went from +4.5% / +12.2% / -11.6% to
-0.1% / +4.2% / +2.1%, 5.7 times tighter. The help page says so in those words, because
somebody reading "weight averaging" will look at Dice, find nothing, and conclude wrongly.
Refused beside callback_cosine_annealing(): a cosine has decayed to its floor by the
time averaging begins, so the average is the last epoch.
segmentation_data(validation = FALSE): say a run has no validation set. The third
answer to the question a path and a split already answered, and it had to be a stated
value rather than an absence, because "I have no validation set" and "I forgot" look
identical and only one of them is a decision. It went in the same argument rather than a
new one, so nothing can contradict it.
segmentation_data(split = ) and detection_data(split = ): divide one training
folder into splits without writing any files. The argument is a list of fractions (two
numbers, or three to cut a test set too) and group_by, which is a required key that
may be NULL. The mistake it prevents leaves no trace: slices of one patient on both
sides make validation measure memory rather than generalisation, and nothing in the output
says so. An omitted key would be that choice made silently; NULL is the same choice made
on purpose. For detection this is the only way to divide one folder, because
split_dataset() writes a masks column.detection_crops(): cut the detected objects out of the images they were found in, at
a fixed size, for handing to a classifier. Crops come from the source image through the
same reader the dataset uses, are rounded outward so the whole box survives, and
letterbox rather than stretch.yolo3(box_loss = "giou"). Measured on BCCD over three seeds, 150 epochs, it is
worse on the metric it targets - matched overlap 0.7929 against 0.8038, established at
2.7 standard errors - and its spread is sixty times wider. It ships because its
coordinate term reads zero when the boxes are right, where the default term cannot reach
zero however right they are. ?yolo3 leads with that table, because the name invites
choosing it and the measured answer is the other way.loss_boundary(): a region loss plus the signed distance to the truth's boundary.
region and alpha default to NULL here and are left out of the request, so the
engine's own defaults apply - alpha = 0.9, which it pins with a test, because at 0.5 the
surface term owns half the objective while the prediction is still random and the volume
bias spread goes to +/-28%. What the loss trades is accuracy per case for being unbiased
over a series: measured over three seeds, the systematic volume bias went from -5.18% to
-0.06% and the per-case absolute error from 16.90% to 22.17%.\examples section but one - 47 of 48 at the time, encoders
being a concept page with no \usage - and a test parses every example in every .Rd,
including inside \dontrun{}, and fails if it names a function that does not exist. What
was at risk was never the functionality; it is the example text drifting from the API.lint_package(), 25 - two of which were defects rather than style.evaluate_images(): one row per image for a detector - n_truth, n_predicted,
matched, missed, spurious, mean_matched_iou. Order by missed for the frames it
cannot see, by mean_matched_iou for the ones where it finds everything and places it
badly. No average precision per image: AP is the area under a ranking's curve and on one
picture it moves on a single box.platypus_spec(task = ) can be stated, and is checked rather than trusted - it can
confirm or refuse, never override. Unset, it is still derived from the constructors.
Required in YAML and optional here is not an inconsistency: task is needed exactly where
the type is not already stated, and segmentation_data() with u_net() can only mean one
thing.detection_data(), yolo3(), detection_anchors(),
plot_boxes(), and predict() returning a list of boxes in each image's own pixels -
not an array, because images differ in size, so does the number of boxes, and a box in a
letterboxed frame cannot be drawn on the photograph it came from.plot_anchors(): the fitted anchors against the boxes they were fitted to. It is in
the vignette twice, on the split they were fitted on and the one they were not.bccd-yolo3 with no training, and
tools/readme_figures.R that reproduces it.u_net(encoder = "resnet34", pretrained = TRUE), with freeze_encoder and
encoder_learning_rate. pretrained is a separate explicit flag so that naming a
backbone never downloads anything nobody asked for. Measured on three datasets,
pretraining never clearly beats the built-in encoder; what it buys is epochs, and what a
swappable encoder buys is capacity where the target is thin. The help page carries the
table.inspect_masks() and platypus_fit(check_masks = FALSE): a declared class appearing
in no mask has a target channel that is zero everywhere, so the model is never shown what
it is being asked to find, and platypus_fit() now refuses on it. A threshold on the
unmatched fraction cannot do this job - on a 0.6% lesion a wholly wrong colormap is
quieter than JPEG artefacts in healthy data.DESCRIPTION had no
Roxygen: list(markdown = TRUE) although every page had been written in markdown since
the rewrite began, so not one cross-reference was a link: 0 to 42 links, 0 to 266 inline
code spans, 36 of 40 pages changed. Nothing in the sources was wrong; nothing was
converting them.predict(space = "source"): map a prediction back out of the model's grid onto the
grid of the scan it came from, undoing the resampling and the crop. Returns a list, and
the type change is the API being honest - scans differ in size, and quietly resizing them
to match is how a mask ends up describing anatomy it was not computed from.weights = "dsbowl-unet" from the registry, hf://owner/repo/file@commit,
or a local path, plus export_weights() for your own. A registry name carries a commit
and the hf:// form requires one, because weights that change under a stable name are the
worst kind of irreproducibility.fit = FALSE, plot_masks() - before any mention of training. First contact should not
need a GPU and an afternoon.weights = "dsbowl-unet" could not work
from R at all. The bridge asked for pyplatypus with no extras, so huggingface_hub was
never in the environment reticulate builds. Nothing in either test suite could have caught
it - the engine's tests install the extra deliberately and the R tests ran where it was
already present.NAMESPACE files
rather than from memory: of 0.1.1's 37 exports, 30 are gone, and three of the seven
survivors are worse than gone because they are still callable and mean something else.The day the package stopped being a bridge and became a surface. Five engine releases, each followed here.
R CMD check jobs - Ubuntu on R devel, release and
oldrel-1, plus macOS and Windows on release - and an engine job that installs the built
package and runs the suite against the engine resolved from PyPI. That last one is the
job that matters: pkgload::load_all() exposes a package's internals, library() does
not, and local work had been using the most permissive of the three ways a test can
reach them.plot_masks(), plot_boxes(), overlay_mask(),
overlay_agreement(), read_masks(), unite_masks(), mask_coverage().series_report() for a folder of slices, whose decision is that a problem is a column,
not an exception - a hundred cases contain a few bad ones, and stopping at the first
turns an afternoon into a week.split_dataset(), split_path()) and evaluate_cases().input_shape = c(64, 64, 32) had been building a 3D U-Net since the first
release; what was missing was reading NIfTI, where orientation is the whole problem, and
labels as a second way to describe a mask, because volumes label with integers.
save_volumes() writes NIfTI carrying the affine of the scan the mask was predicted
from - without it a mask cannot be laid over its scan by any viewer, and the file looks
perfectly normal while being wrong. mask_volume() reports millilitres, because a voxel
count is meaningless outside one scanner.target_spacing: resample to a common voxel size. What makes it necessary is not
slice thickness but field of view - forty slices of 1 mm is 40 mm of patient and forty of
2.5 mm is 100 mm, which is the ordinary state of clinical data.channels_from: one channel per file, stating the order and never inferring it. A
model trained with FLAIR in channel one and used where channel one is T1 answers
plausibly, with the right shape and range, and nothing downstream can tell.available_transforms(rank = 3) to say what is usable - 97 of
albumentations' transforms accept a volume and the rest raise from inside the library.segmentation_data(window = ) replaced dicom_window, since NIfTI needs the same
thing. The old name is still accepted; giving both is an error.The package exists.
py_require() plus uv, resolving an isolated ephemeral environment at
library(rplatypus) - deliberately not the shared-Python arrangement that broke this
package's Keras stack in 2020. platypus_status() says what it found rather than leaving
a failed import to be guessed at, and PLATYPUS_ENGINE_PATH points the bridge at a source
tree during development.platypus_spec(), built either from arguments or from a YAML file and producing the
same thing. One pipeline, two constructors.platypus_fit(), evaluate(), predict(), training_history(), and the engine's
complaints translated into R conditions rather than passed through as Python tracebacks.The last release of the TensorFlow package, kept here for the record rather than as an
ancestor of the entries above. It built Keras models through the R keras package: yolo3()
and darknet53() with load_darknet_weights() for the original Darknet weights, a plain
u_net(), segmentation_generator(), loss_dice(), the COCO and VOC colormaps, and
readers for LabelMe JSON and Pascal VOC XML.
It is still installable and still pinned:
remotes::install_github("maju116/[email protected]")
Its source is on the master branch and the issues filed against it stay open. What has
aged is its dependencies rather than its code - the pinned TensorFlow no longer installs on
a current Python, which is the reason for a rewrite rather than a consequence of one.
This was in the README until 2026-10-07 - seventy lines between ## Installing and the first
example, about a version the reader does not have, before the one they do. It is here because
this file is where R keeps what changed between versions, and because the renames table below
was a second copy of one the README already carried; the fuller of the two survived.
Code written against the 2020 release will not run on this one. That version built Keras
models through the R keras package and handed them back to you; this one describes a whole
run as a specification and computes it in PyTorch. Of the 37 functions and objects the old
version exported, 30 are gone — including all of yolo3(), darknet53(),
load_darknet_weights(), get_boxes(), non_max_suppression(), plot_boxes(),
segmentation_generator() and the annotation readers.
Seven names survive, and three of them are the ones to watch, because they are still exported, still callable, and mean something different:
| name | 0.1.1 | now |
|---|---|---|
| u_net() | u_net(net_h, net_w, grayscale, ...) → a Keras model | u_net(name, input_shape, ...) → one model in a specification |
| plot_masks() | plot_masks(images_paths, masks, labels, colormap) | plot_masks(images, prediction, truth, ...) |
| loss_dice() | no arguments, returns a Keras loss | loss_dice(smooth = 1), returns a loss to name in a specification |
The other four — binary_colormap, voc_colormap, binary_labels, voc_labels — are the
same data as before.
Object detection is back, and the names are different. The old package handed you a Keras YOLOv3; this one describes a detector in a specification like everything else. So those five names are gone rather than moved, and what replaces them is:
| 0.1.1 | now |
|---|---|
| yolo3(), darknet53() | yolo3() as one entry in platypus_spec() - same name, a specification rather than a model |
| load_darknet_weights() | weights = "bccd-yolo3", or a path, or hf://owner/repo/file@commit |
| get_boxes(), non_max_suppression() | done inside predict(), which returns boxes already suppressed and in each image's own pixels |
| plot_boxes() | plot_boxes() - same name, takes what predict() returns |
The annotation readers are gone as functions and are now what detection_data() reads for
you: Pascal VOC XML or LabelMe JSON, named rather than guessed.
The old release is still there and still installable, pinned:
remotes::install_github("maju116/[email protected]")
Its source is on the master branch, and the issues filed against it stay open. What has aged
is its dependencies rather than its code: the pinned TensorFlow no longer installs on a current
Python, which is the reason for a rewrite rather than a consequence of one.