Skip to content

Better support for tuple and complex variables in fits, data and inits - #1287

Open
jgabry wants to merge 6 commits into
v1.0from
tuple-complex
Open

jgabry wants to merge 6 commits into
v1.0from
tuple-complex

Conversation

@jgabry

@jgabry jgabry commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Submission Checklist

  • Run unit tests
  • Declare copyright holder and agree to license (see below)

Summary

AI was used in writing the code for this PR and the summary below. I have reviewed all code.


Supersedes #1174 and reuses its test model. Closes #925.

CmdStan names a tuple element's columns with a colon (b_tuple:2.1.1)
and a complex number's parts with .real and .imag. Our name repair
and variable_dims() assumed every column was name.i.j, so a fit with
such variables warned about NA sizes, its metadata was wrong and a
variable filter could not select a tuple. Two more layers had the same
gap: the model methods rebuilt constrained values through
utils::relist() from the compiled model's own dotted names, which
mislabel arrays of tuples, and write_stan_json() could not write a
tuple at all, so tuple data and inits were unusable.

What changes:

  • Column names are repaired piece by piece and keep the colons:
    b_tuple:2[1,1], arr_pair[1]:2, z[real], zv[1,imag]. This is what
    posterior already does with them. A variable is the name before the
    first [ or : everywhere names are filtered or sized, so
    fit$draws("b_tuple") selects all of a tuple's columns.
    stan_variable_sizes gives a complex variable a trailing 2 and a
    tuple the size of its outer array.
  • $constrain_variables() rebuilds its result from the model's
    constrained_param_names(): a complex value comes back as an R
    complex number or array, a tuple as an unnamed list of its elements.
    $unconstrain_variables() and $unconstrain_draws() accept the same
    shapes. $variable_skeleton() is removed along with the C++ that fed
    it; utils::relist() is no longer needed. The one shape change for
    existing models is that a 1-d variable now comes back as a 1-d array
    instead of a bare vector.
  • write_stan_json() gains a variables argument taking the
    declarations from $variables() or variables_stan_file(). With them
    an unnamed list is written as a tuple; without them it is still
    converted to an array as before. Complex values are written as
    [re, im] pairs. The fitting methods pass the model's declarations
    for data and inits, which also moves the per-variable data
    conversions from process_data() into the writer, and init = fit
    carries tuple parameters over.

One caveat found while writing the test model: CmdStan 2.40's JSON
reader misreads a complex value inside an array of tuples (a bare
tuple(real, complex) and complex_vector read fine). The test model's
array[2] tuple(real, real) data avoids it and the model notes why.
See stan-dev/stan#3437.

Tests: unit tests for the name repair, sizes, filters and the
flatten/unflatten pair; write_stan_json() with tuples and complex
values; and test-tuple-complex.R, one fit of the model exercising the
metadata, filters, data round trip, inits from a fit and from a list,
and the model methods.

Copyright and Licensing

Please list the copyright holder for the work you are submitting
(this will be you or your assignee, such as a university or company):
Jonah Gabry

By submitting this pull request, the copyright holder is agreeing to
license the submitted work under the following licenses:

CmdStan names a tuple element's columns with a colon (b_tuple:2.1.1)
and a complex number's parts with .real and .imag. The name repair and
variable_dims() in csv.R assumed every column was name.i.j, so a fit
with such variables warned about NA sizes, its metadata was wrong and
a variable filter could not select a tuple. The model methods rebuilt
constrained values through utils::relist() from the compiled model's
own dotted names, which mislabel arrays of tuples, and write_stan_json()
had no way to write a tuple at all.

Column names are now repaired piece by piece, keeping the colons
(b_tuple:2[1,1], arr_pair[1]:2, z[real]), and the variable is the name
before the first bracket or colon everywhere a name is filtered or
sized. A complex variable's size has a trailing 2 for its parts and a
tuple's is that of its outer array. constrain_variables() rebuilds its
result from the model's constrained_param_names(), which replaces the
skeleton, so variable_skeleton() and the param metadata C++ are gone.
write_stan_json() gains a variables argument with the model's
declarations and writes a tuple from an unnamed list; process_data()
and the init writers pass the declarations, which moves the
per-variable data conversions into the writer.

The test model is PR #1174's with a data block added. Its array of
tuples holds no complex value because CmdStan 2.40's JSON reader
misreads a complex number inside an array of tuples.

Closes #925.
@codecov-commenter

codecov-commenter commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.02956% with 4 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (v1.0@f4bb79c). Learn more about missing BASE report.

Files with missing lines Patch % Lines
R/csv.R 96.00% 4 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             v1.0    #1287   +/-   ##
=======================================
  Coverage        ?   93.13%           
=======================================
  Files           ?       19           
  Lines           ?     7372           
  Branches        ?        0           
=======================================
  Hits            ?     6866           
  Misses          ?      506           
  Partials        ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Codex's review of the tuple and complex support found four defects.
flatten_variables() stacked the real and imaginary matrices of a 2-d
complex value instead of pairing the parts of each element, so every
complex matrix went through unconstrain_variables() and write_stan_json()
permuted. unflatten_leaves() laid values out in the order the columns
arrived and read the dims off the last column, so a draws object with
reordered or missing columns gave a permuted, truncated or recycled init
and a tuple element with no columns shifted the later ones down.
nest_cells() built seq(i, 0, by = 2) for an array of tuples with a zero
extent after a nonzero one. list_to_array() rejected complex elements.

Columns are now placed by their parsed indices, a missing element is an
error, tuple slots are keyed by number and an empty one comes back as
numeric(0). Trailing empty elements are padded from the declared tuple
length when the caller has $variables(). Zero-size variables with no
columns stay omitted from constrain_variables(), as they are from the
draws, and the roxygen for the three transform methods now says so.

Part of #925.
When a complex variable's columns are incomplete, complex() recycles
the parts it has, so a draws object missing z[1,imag] initialized z
with another element's value. unflatten_leaves() now requires one real
and one imaginary column per element, and a complex array goes
through the same per-cell check as an array of tuples.

pad_tuple() iterated over the declaration, so a draws object with more
tuple elements than the model declares lost the extra ones before
write_stan_json() could reject them. It now pads up to the declared
length and keeps the rest.

A tuple element with no columns still comes back as numeric(0), which
is the right value for a zero-length vector but has the wrong rank for
an empty matrix. The declarations carry no sizes, so the shape can't
be recovered; the return value of constrain_variables() documents it.

Part of #925.
@jgabry
jgabry marked this pull request as ready for review September 24, 2026 13:38
@jgabry jgabry changed the title Better support tuple and complex variables in fits, data and inits Better support for tuple and complex variables in fits, data and inits Sep 24, 2026
The file compiles a model, so it needs the skip_on_cran() line every
CmdStan test file gets in #1288; adding it here saves that PR a
follow-up once both are on v1.0.

Part of #925.
@jgabry

jgabry commented Sep 24, 2026 •

Copy link
Copy Markdown
Member Author

@SteveBronder this closes #925, which is an issue you opened a while ago. Do you have time to briefly try out this branch just to see if it behaves as you'd expect for complex and tuple types?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants