Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Display and Debug outputs concise and consistent #1119

Open
wants to merge 16 commits into
base: dev
Choose a base branch
from

Conversation

owenbrooks
Copy link

@owenbrooks owenbrooks commented Jun 11, 2022

Summary

  • Removed type names from Display implementations (they remain in/were added to Debug)
  • Changed vector-based geometry types (Translation, Scale) to display on a single line by default (Was already the case for Point). Pretty-printing as a multiline vector can be enabled using 'alternative' formatting i.e. println!("{point:#}")
  • Consistently wrapped struct displays in {} and vector displays in []
  • Precision parameter now applies to all geometry types e.g. println!("{point:.2}")
  • Other formatting parameters such as padding apply recursively to everything that is not pretty-printed using the main multiline printing macro
  • Quaternions now display as real + imaginary components i.e. 2 + 3i + 4j + 1k and debug as {x: 2, y: 3, z: 4, w: 1} (Unit and UnitDualQuaternions still display as angle & axis for rotation)
  • Removed trailing newlines from matrix formatting - previously, formatting matrices would add 2 newlines after the printing of the bottom of the matrix. This reduces flexibility and takes up more vertical space than necessary, though I'd be happy to let this go.
Differences when printing (expand)
Before After
Point - normal
{1.12345678, 2.12345678, 3.12345678}

Point - normal
[1.12345678, 2.12345678, 3.12345678]

Point - alternate
{1.12345678, 2.12345678, 3.12345678}

Point - alternate

  ┌            ┐
  │ 1.12345678 │
  │ 2.12345678 │
  │ 3.12345678 │
  └            ┘

Point - debug
[1.12345678, 2.12345678, 3.12345678]

Point - debug
Point [1.12345678, 2.12345678, 3.12345678]

Scale - normal
Scale {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  └       ┘

}


Scale - normal
[1.0, 2.0]

Scale - alternate
Scale {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  └       ┘

}


Scale - alternate

  ┌   ┐
  │ 1 │
  │ 2 │
  └   ┘

Scale - debug
[1.0, 2.0]

Scale - debug
Scale [1.0, 2.0]

Translation - normal
Translation {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  │ 3.000 │
  └       ┘

}


Translation - normal
[1.0, 2.0, 3.0]

Translation - alternate
Translation {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  │ 3.000 │
  └       ┘

}


Translation - alternate

  ┌   ┐
  │ 1 │
  │ 2 │
  │ 3 │
  └   ┘

Translation - debug
[1.0, 2.0, 3.0]

Translation - debug
Translation [1.0, 2.0, 3.0]

Matrix - normal

  ┌         ┐
  │ 1.2 2.4 │
  │ 3.1 4.5 │
  └         ┘



Matrix - normal

  ┌         ┐
  │ 1.2 2.4 │
  │ 3.1 4.5 │
  └         ┘

Matrix - debug
[[1.2, 3.1], [2.4, 4.5]]

Matrix - debug
[[1.2, 3.1], [2.4, 4.5]]

UnitQuaternion - normal
UnitQuaternion angle: 3.141592653589793 − 
axis: (0, 1, 0)

(linebreak added just for better viewing in this table)
UnitQuaternion - normal
{ angle: 3.141592653589793, axis: (0, 1, 0) }

UnitQuaternion - debug
[0.0, 1.0, 0.0, 6.123233995736766e-17]

UnitQuaternion - debug
Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 6.123233995736766e-17 }

Isometry - normal
Isometry {
Translation {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  │ 3.000 │
  └       ┘

}
UnitQuaternion angle: 3.141592653589793 − axis: (0, 1, 0)}


Isometry - normal
{ translation: [1.0, 2.0, 3.0], rotation: 
{ angle: 3.142, axis: (0.000, 1.000, 0.000) } }

(linebreak added just for better viewing in this table)
Isometry - debug
Isometry { rotation: [0.0, 1.0, 0.0, 
6.123233995736766e-17], translation: [1.0, 2.0, 3.0] }

(linebreak added just for better viewing in this table)
Isometry - debug
Isometry { rotation: Quaternion { x: 0.0, y: 1.0, z: 0.0, 
w: 6.123233995736766e-17 }, translation: Translation [1.0, 2.0, 3.0] }

(linebreak added just for better viewing in this table)
UnitComplex - normal
UnitComplex angle: 0.15

UnitComplex - normal
{ angle: 0.15 }

UnitComplex - debug
Complex { re: 0.9887710779360422, im: 0.14943813247359922 }

UnitComplex - debug
Complex { re: 0.9887710779360422, im: 0.14943813247359922 }

Similarity - normal
Similarity {
Isometry {
Translation {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  │ 3.000 │
  └       ┘

}
UnitQuaternion angle: 3.141592653589793 − axis: (0, 1, 0)}
Scaling: 0.100}

(linebreak added just for better viewing in this table)
Similarity - normal
{ { translation: [1.000, 2.000, 3.000], rotation: 
{ angle: 3.142, axis: 
(0.000, 1.000, 0.000) } }, scaling: 0.100 }

(linebreaks added just for better viewing in this table)
Similarity - debug
Similarity { isometry: Isometry { rotation: [
  0.0, 1.0, 0.0, 6.123233995736766e-17], 
translation: [1.0, 2.0, 3.0] }, scaling: 0.1 }

(linebreaks added just for better viewing in table)
Similarity - debug
Similarity { isometry: Isometry { rotation: 
Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 
6.123233995736766e-17 }, 
translation: Translation [1.0, 2.0, 3.0] }, 
scaling: 0.1 }

(linebreaks added just for better viewing in table)
Quaternion - normal
Quaternion 1 − (2, 3, 4)

Quaternion - normal
2 + 3i + 4j + 1k

Quaternion - debug
[2.0, 3.0, 4.0, 1.0]

Quaternion - debug
Quaternion { x: 2.0, y: 3.0, z: 4.0, w: 1.0 }

@RReverser
Copy link

I definitely like the new Display without type names, as well as usage of # for expanded form. Few questions though:

  1. Why is e.g. Scale normal using [1.0, 2.0] but Scale alternative [1, 2] (the .0 part)? Shouldn't it be either the same or even the other way around? Ideally we should just use normal display formatting for numbers, which would show 1, 2 for both.
  2. Matrix - normal feels a bit inconsistent compared to other examples, as in, if we use # for multiline format, it seems natural for "normal" variant to use single-line form of matrix too. Maybe something like {{1,2,3}, {4,5,6}}. Or maybe it's too much of a breaking change to introduce?
  3. Shouldn't Matrix debug form show its type name?
  4. What about debug alternate form, aka {:#?}? Haven't seen examples of those.

@owenbrooks owenbrooks force-pushed the formatting_fixes branch 2 times, most recently from 5df8a77 to 37de790 Compare June 11, 2022 12:35
@Andlon
Copy link
Sponsor Collaborator

Andlon commented Jun 11, 2022

Thanks for working on this @owenbrooks!

I'm very short on time today but I wanted to make some initial comments; I will probably have more later, when I have the time.

I'll restrict my comments to the Debug output of Matrix right now, because it is horribly broken and problematic at the moment, see also #1071. The crux of the issue is that it defers to the Debug impl of its storage, which is deeply problematic. Your example with Matrix uses a statically allocated matrix (e.g. with matrix!), but you'll see that if you print a dynamically allocated matrix the output is wildly different.

It would be great if we could resolve this while we're at it, as the resolution may impact the other geometric types as well.

In short:

  • printing of dynamically allocated matrices will just default to an inferred Debug impl of VecStorage (see Debug output is broken for dynamic matrices #1071 for an example).
  • printing of statically allocated matrices ends up using the Debug output of ArrayStorage, which defers to the Debug impl of its internal array. But this array is column-major! Which is very confusing and unexpected, and we've had many users both on Discord and in various GitHub issues be confused about this fact.

To resolve both these issues, I suggest that we use the same syntax that we use for matrix construction - through matrix! and dmatrix!. That is, we delimit columns by commas and rows by semi-colons. This has several benefits:

  • You can directly take the debug output and copy-paste it into your code, using matrix! or dmatrix!. For me this is a big deal, because it's something I do quite often (for example create a unit test from a failing real-world example).
  • Delimiting by semi-colons means that we can print any matrix on a single line by default, and use # for multi-line formatting, which I believe resolves one of @RReverser's concerns.

Note that this means that vectors will always print as e.g. [1; 2; 3], since they are matrices with dimensions Nx1. This appears to be the right thing to do in any case, because we cannot distinguish between a Nx1 matrix and a column vector, because they are literally the same type.

Finally, I think there should not be a type name for the matrix in the debug output. Vec also does not include its own name. It's usually obvious from the context that we are working with a matrix/vector, so it's not necessary to explicitly tag it as such, and indeed tagging it makes the debug output more verbose, which is problematic in practice when you are printing large amounts of data or complex structs. This same argument does not necessarily hold for all of the geometry types, however.

@owenbrooks
Copy link
Author

  1. Why is e.g. Scale normal using [1.0, 2.0] but Scale alternative [1, 2] (the .0 part)? Shouldn't it be either the same or even the other way around? Ideally we should just use normal display formatting for numbers, which would show 1, 2 for both.

Good point. It was forwarding to Debug for normal but using iterating through the values and using write! instead gets that normal display formatting now.

  1. Matrix - normal feels a bit inconsistent compared to other examples, as in, if we use # for multiline format, it seems natural for "normal" variant to use single-line form of matrix too. Maybe something like {{1,2,3}, {4,5,6}}. Or maybe it's too much of a breaking change to introduce?

My feeling is that as a matrix library its nice to see matrices multi-line by default, and these vector-based types are an exception where single line is more convenient, but the alternate form is still there if needed.

  1. Shouldn't Matrix debug form show its type name?

True, I've added that now.

  1. What about debug alternate form, aka {:#?}? Haven't seen examples of those.

Here they are:

Point - debug alternate
Point [
    1.12345678,
    2.12345678,
    3.12345678,
]

Scale - debug alternate
Scale [
    1.0,
    2.0,
]

Translation - debug alternate
Translation [
    1.0,
    2.0,
    3.0,
]

Matrix - debug alternate
Matrix [
    [
        1.2,
        3.1,
    ],
    [
        2.4,
        4.5,
    ],
]

UnitQuaternion - debug alternate
Quaternion {
    x: 0.0,
    y: 1.0,
    z: 0.0,
    w: 6.123233995736766e-17,
}

Isometry - debug alternate
Isometry {
    rotation: Quaternion {
        x: 0.0,
        y: 1.0,
        z: 0.0,
        w: 6.123233995736766e-17,
    },
    translation: Translation [
        1.0,
        2.0,
        3.0,
    ],
}

UnitComplex - debug alternate
Complex {
    re: 0.9887710779360422,
    im: 0.14943813247359922,
}

Similarity - debug alternate
Similarity {
    isometry: Isometry {
        rotation: Quaternion {
            x: 0.0,
            y: 1.0,
            z: 0.0,
            w: 6.123233995736766e-17,
        },
        translation: Translation [
            1.0,
            2.0,
            3.0,
        ],
    },
    scaling: 0.1,
}

Quaternion - debug alternate
Quaternion {
    x: 2.0,
    y: 3.0,
    z: 4.0,
    w: 1.0,
}

UnitDualQuaternion - debug alternate
DualQuaternion {
    real: Quaternion {
        x: 0.0,
        y: 1.0,
        z: 0.0,
        w: 6.123233995736766e-17,
    },
    dual: Quaternion {
        x: 0.0,
        y: 0.0,
        z: 0.0,
        w: 0.0,
    },
}

@RReverser
Copy link

  • You can directly take the debug output and copy-paste it into your code, using matrix! or dmatrix!. For me this is a big deal, because it's something I do quite often (for example create a unit test from a failing real-world example).

Oh yes, it's something I definitely wanted in the past but wasn't sure if in scope of this PR. Being able to copy-paste debug output into matrix construction would be a big deal. Perhaps we could follow the same for point & vector too.

@Andlon
Copy link
Sponsor Collaborator

Andlon commented Jun 11, 2022

@RReverser: vectors are matrices, so they'd be printed as e.g. [1; 2; 3], which is not currently compatible with the vector! macro (but you could use the matrix! macro). However, we could extend the (d)vector! macros to accept that syntax too.

@Andlon
Copy link
Sponsor Collaborator

Andlon commented Jun 11, 2022

Sorry, I didn't mean to suggest that you have to resolve #1071 in this PR - we don't have to increase the scope. We could also do it in several stages, as separate PRs, but in any case I think it's worth thinking about to make sure that everything ends up consistent in the end!

@RReverser
Copy link

@RReverser: vectors are matrices, so they'd be printed as e.g. [1; 2; 3], which is not currently compatible with the vector! macro (but you could use the matrix! macro). However, we could extend the (d)vector! macros to accept that syntax too.

Ah right, that's unfortunate. Ideally vector! would both accept and distinguish between those two forms so that vector![1,2,3] would produce a RowVector, but vector![1;2;3] would produce a (column) Vector, but that would be, again, a breaking change :(

@Andlon
Copy link
Sponsor Collaborator

Andlon commented Jun 11, 2022

@RReverser: vectors are matrices, so they'd be printed as e.g. [1; 2; 3], which is not currently compatible with the vector! macro (but you could use the matrix! macro). However, we could extend the (d)vector! macros to accept that syntax too.

Ah right, that's unfortunate. Ideally vector! would both accept and distinguish between those two forms so that vector![1,2,3] would produce a RowVector, but vector![1;2;3] would produce a (column) Vector, but that would be, again, a breaking change :(

What you're describing is exactly the behavior of matrix! ;-)

vector! really only exists to make it more explicit that you're building a (column) vector, because vectors and matrices often serve different purposes in applications.

@RReverser
Copy link

What you're describing is exactly the behavior of matrix! ;-)

vector! really only exists to make it more explicit that you're building a (column) vector, because vectors and matrices often serve different purposes in applications.

That's fair. I guess the suggestion to output macro form applies only to {d}matrix! and point! then.

@Ralith
Copy link
Collaborator

Ralith commented Jun 11, 2022

@owenbrooks

My feeling is that as a matrix library its nice to see matrices multi-line by default, and these vector-based types are an exception where single line is more convenient, but the alternate form is still there if needed.

This is reasonable, but IMO consistency is more important. It's really weird for one type to have exactly the opposite behavior of the others. I agree with @Andlon that we should standardize on "alternate form" meaning fancy 2D layout, which is also consistent with the stdlib use of it in Debug to mean "pretty print".

@owenbrooks
Copy link
Author

This is reasonable, but IMO consistency is more important. It's really weird for one type to have exactly the opposite behavior of the others. I agree with @Andlon that we should standardize on "alternate form" meaning fancy 2D layout, which is also consistent with the stdlib use of it in Debug to mean "pretty print".

Wanting consistency between the types does make sense. I would lean towards 2D printing by default for Display as it works currently in nalgebra (and making Point consistent with this).

Looking at how printing is handled in projects like numpy, eigen, ndarray, matlab, R, they all print matrices multi line at least by default (though vectors tend to be row vectors and so take up a single line). Since nalgebra is focussed on 2D matrices, printing in 2D feels like a very canonical string representation. It's harder to see 2D patterns when the matrix is squished onto one line.

If we make the Debug output semicolon-delimited row-wise would that be usable enough for the case when single-line is desirable? Could do without alternate in that case. Otherwise, could switch it around so that “alternate” forces single line for Display.

@Andlon I will undo the addition of "Matrix " to debug output. I will also take a look at fixing that matrix Debug, it may become a separate PR.

@RReverser
Copy link

and making Point consistent with this

I've said this before, but I'd really want to keep single-line Points for short Display. The fact they're columns in nalgebra feels almost like implementation detail, since in most contexts people would just want to see their coordinates.

@Ralith
Copy link
Collaborator

Ralith commented Jun 12, 2022

If we make the Debug output semicolon-delimited row-wise would that be usable enough for the case when single-line is desirable?

That suits me fine, at least. Having an alternate form mode that's exactly equivalent to Debug feels a bit superfluous, and I don't care which is which so long as there's consistency and a concise form is readily available.

@RReverser

I'd really want to keep single-line Points for short Display

Are your requirements not addressed by just using Debug? I have little to no interest in the 2D representations myself but I'm perfectly happy just using Debug to resolve that. I feel like it would be a bit inconsistent for Vector to be vertical while a Point with the same data is horizontal, and idiomatic nalgebra geometric code will mix both heavily, so giving a concise form for only one seems of limited use.

@RReverser
Copy link

I feel like it would be a bit inconsistent for Vector to be vertical while a Point with the same data is horizontal

Personally, I think that's fine since semantically they're different types. If anything, I agree that consistency is crucial for Debug formatting, but Display is supposed to be easily human-readable first and foremost, and for that it should have some leeway to write whatever makes most sense in the context.

Debug would be fine for a workaround, but we are adding type names consistently to all types, and it would be more verbose than Display. That's expected, Debug is pretty much always meant to be the more verbose option, but it also means we should leverage Display for the more readable option.

@owenbrooks
Copy link
Author

I've reworked it a bit as follows:

  • Display is 2D pretty-print by default and does not include type names.
  • Display alternate prints matrices and geometry-vector types on a single line. It does not include the type name.
    • Matrix/vector rows are semicolon-delimited e.g. [1.0, 2.0; 3.0, 4.0],
    • Geometry-vector types (Point, Translation, and Scale) are only comma-delimited e.g. [0.1, 0.2, 0.3]
  • Debug output includes type names and dimensionality and is mostly derived. However, it doesn't forward matrix Debug directly to Storage, instead it prints matrix data with semicolon-delimited rows. This does make Debug output more verbose than previously for static matrices, but it ensures all information is there and consistent between types, and there is now the concise option using Display alternate.

Requires a couple of changes to tests still.

This should fix #1071 and #898

New format output
==================
Point
==================
Normal

  ┌            ┐
  │ 1.12345678 │
  │ 2.12345678 │
  │ 3.12345678 │
  └            ┘

Alternate
[1.12345678, 2.12345678, 3.12345678]

Debug
OPoint { coords: Matrix { data: [1.12345678; 2.12345678; 3.12345678], nrows: 3, ncols: 1 } }

Debug alternate
OPoint {
    coords: Matrix { 
        data: [1.12345678; 
               2.12345678; 
               3.12345678], 
        nrows: 3, 
        ncols: 1 
    },
}

==================
Scale
==================
Normal

  ┌   ┐
  │ 1 │
  │ 2 │
  └   ┘

Alternate
[1, 2]

Debug
Scale { vector: Matrix { data: [1.0; 2.0], nrows: 2, ncols: 1 } }

Debug alternate
Scale {
    vector: Matrix { 
        data: [1.0; 
               2.0], 
        nrows: 2, 
        ncols: 1 
    },
}

==================
Translation
==================
Normal

  ┌   ┐
  │ 1 │
  │ 2 │
  │ 3 │
  └   ┘

Alternate
[1, 2, 3]

Debug
Translation { vector: Matrix { data: [1.0; 2.0; 3.0], nrows: 3, ncols: 1 } }

Debug alternate
Translation {
    vector: Matrix { 
        data: [1.0; 
               2.0; 
               3.0], 
        nrows: 3, 
        ncols: 1 
    },
}

==================
Matrix
==================
Normal

  ┌         ┐
  │ 1.2 2.4 │
  │ 3.1 4.5 │
  └         ┘

Alternate
[1.2, 2.4; 3.1, 4.5]

Debug
Matrix { data: [1.2, 2.4; 3.1, 4.5], nrows: 2, ncols: 2 }

Debug alternate
Matrix { 
    data: [1.2, 2.4; 
           3.1, 4.5], 
    nrows: 2, 
    ncols: 2 
}

==================
UnitQuaternion
==================
Normal
{ angle: 3.141592653589793, axis: (0, 1, 0) }

Alternate
{ angle: 3.141592653589793, axis: (0, 1, 0) }

Debug
Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 6.123233995736766e-17 }

Debug alternate
Quaternion {
    x: 0.0,
    y: 1.0,
    z: 0.0,
    w: 6.123233995736766e-17,
}

==================
Isometry
==================
Normal
{ translation: [1, 2, 3], rotation: { angle: 3.141592653589793, axis: (0, 1, 0) } }

Debug
Isometry { rotation: Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 6.123233995736766e-17 }, translation: Translation { vector: Matrix { data: [1.0; 2.0; 3.0], nrows: 3, ncols: 1 } } }

Debug alternate
Isometry {
    rotation: Quaternion {
        x: 0.0,
        y: 1.0,
        z: 0.0,
        w: 6.123233995736766e-17,
    },
    translation: Translation {
        vector: Matrix { 
            data: [1.0; 
                   2.0; 
                   3.0], 
            nrows: 3, 
            ncols: 1 
        },
    },
}

==================
UnitComplex
==================
Normal
UnitComplex angle: 0.15

Debug
Complex { re: 0.9887710779360422, im: 0.14943813247359922 }

Debug alternate
Complex {
    re: 0.9887710779360422,
    im: 0.14943813247359922,
}

==================
Similarity
==================
Normal
{ translation: [1, 2, 3], rotation: { angle: 3.141592653589793, axis: (0, 1, 0) }, scaling: 0.1 }

Debug
Similarity { isometry: Isometry { rotation: Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 6.123233995736766e-17 }, translation: Translation { vector: Matrix { data: [1.0; 2.0; 3.0], nrows: 3, ncols: 1 } } }, scaling: 0.1 }

Debug alternate
Similarity {
    isometry: Isometry {
        rotation: Quaternion {
            x: 0.0,
            y: 1.0,
            z: 0.0,
            w: 6.123233995736766e-17,
        },
        translation: Translation {
            vector: Matrix { 
                data: [1.0; 
                       2.0; 
                       3.0], 
                nrows: 3, 
                ncols: 1 
            },
        },
    },
    scaling: 0.1,
}

==================
Quaternion
==================
Normal
2 + 3i + 4j + 1k

Debug
Quaternion { x: 2.0, y: 3.0, z: 4.0, w: 1.0 }

Debug alternate
Quaternion {
    x: 2.0,
    y: 3.0,
    z: 4.0,
    w: 1.0,
}

==================
UnitDualQuaternion
==================
Normal
{ translation: [0, 0, 0], rotation: { angle: 3.141592653589793, axis: (0, 1, 0) } }

Debug
DualQuaternion { real: Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 6.123233995736766e-17 }, dual: Quaternion { x: 0.0, y: 0.0, z: 0.0, w: 0.0 } }

Debug alternate
DualQuaternion {
    real: Quaternion {
        x: 0.0,
        y: 1.0,
        z: 0.0,
        w: 6.123233995736766e-17,
    },
    dual: Quaternion {
        x: 0.0,
        y: 0.0,
        z: 0.0,
        w: 0.0,
    },
}

Old output for reference
==================
Point
==================
Normal
{1.12345678, 2.12345678, 3.12345678}

Alternate
{1.12345678, 2.12345678, 3.12345678}

Debug
[1.12345678, 2.12345678, 3.12345678]

Debug alternate
[
    1.12345678,
    2.12345678,
    3.12345678,
]

==================
Scale
==================
Normal
Scale {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  └       ┘

}


Alternate
Scale {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  └       ┘

}


Debug
[1.0, 2.0]

Debug alternate
[
    1.0,
    2.0,
]

==================
Translation
==================
Normal
Translation {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  │ 3.000 │
  └       ┘

}


Alternate
Translation {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  │ 3.000 │
  └       ┘

}


Debug
[1.0, 2.0, 3.0]

Debug alternate
[
    1.0,
    2.0,
    3.0,
]

==================
Matrix
==================
Normal

  ┌         ┐
  │ 1.2 2.4 │
  │ 3.1 4.5 │
  └         ┘



Alternate

  ┌         ┐
  │ 1.2 2.4 │
  │ 3.1 4.5 │
  └         ┘



Debug
[[1.2, 3.1], [2.4, 4.5]]

Debug alternate
[
    [
        1.2,
        3.1,
    ],
    [
        2.4,
        4.5,
    ],
]

==================
UnitQuaternion
==================
Normal
UnitQuaternion angle: 3.141592653589793 − axis: (0, 1, 0)

Alternate
UnitQuaternion angle: 3.141592653589793 − axis: (0, 1, 0)

Debug
[0.0, 1.0, 0.0, 6.123233995736766e-17]

Debug alternate
[
    0.0,
    1.0,
    0.0,
    6.123233995736766e-17,
]

==================
Isometry
==================
Normal
Isometry {
Translation {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  │ 3.000 │
  └       ┘

}
UnitQuaternion angle: 3.141592653589793 − axis: (0, 1, 0)}


Debug
Isometry { rotation: [0.0, 1.0, 0.0, 6.123233995736766e-17], translation: [1.0, 2.0, 3.0] }

Debug alternate
Isometry {
    rotation: [
        0.0,
        1.0,
        0.0,
        6.123233995736766e-17,
    ],
    translation: [
        1.0,
        2.0,
        3.0,
    ],
}

==================
UnitComplex
==================
Normal
UnitComplex angle: 0.15

Debug
Complex { re: 0.9887710779360422, im: 0.14943813247359922 }

Debug alternate
Complex {
    re: 0.9887710779360422,
    im: 0.14943813247359922,
}

==================
Similarity
==================
Normal
Similarity {
Isometry {
Translation {

  ┌       ┐
  │ 1.000 │
  │ 2.000 │
  │ 3.000 │
  └       ┘

}
UnitQuaternion angle: 3.141592653589793 − axis: (0, 1, 0)}
Scaling: 0.100}


Debug
Similarity { isometry: Isometry { rotation: [0.0, 1.0, 0.0, 6.123233995736766e-17], translation: [1.0, 2.0, 3.0] }, scaling: 0.1 }

Debug alternate
Similarity {
    isometry: Isometry {
        rotation: [
            0.0,
            1.0,
            0.0,
            6.123233995736766e-17,
        ],
        translation: [
            1.0,
            2.0,
            3.0,
        ],
    },
    scaling: 0.1,
}

==================
Quaternion
==================
Normal
Quaternion 1 − (2, 3, 4)

Debug
[2.0, 3.0, 4.0, 1.0]

Debug alternate
[
    2.0,
    3.0,
    4.0,
    1.0,
]

==================
UnitDualQuaternion
==================
Normal
UnitDualQuaternion translation: 
  ┌   ┐
  │ 0 │
  │ 0 │
  │ 0 │
  └   ┘

 − angle: 3.141592653589793 − axis: (0, 1, 0)

Debug
DualQuaternion { real: [0.0, 1.0, 0.0, 6.123233995736766e-17], dual: [0.0, 0.0, 0.0, 0.0] }

Debug alternate
DualQuaternion {
    real: [
        0.0,
        1.0,
        0.0,
        6.123233995736766e-17,
    ],
    dual: [
        0.0,
        0.0,
        0.0,
        0.0,
    ],
}
New format: static/dynamic matrices, ArrayStorage and VecStorage
==========================
Static Matrix
==========================
Display

  ┌         ┐
  │ 1 2 3 4 │
  │ 5 6 7 8 │
  └         ┘

Display Alternate
[1, 2, 3, 4; 5, 6, 7, 8]

Debug
Matrix { data: [1.0, 2.0, 3.0, 4.0; 5.0, 6.0, 7.0, 8.0], nrows: 2, ncols: 4 }

Alternate
Matrix { 
    data: [1.0, 2.0, 3.0, 4.0; 
           5.0, 6.0, 7.0, 8.0], 
    nrows: 2, 
    ncols: 4 
}

==========================
Dynamic Matrix
==========================
Display

  ┌         ┐
  │ 1 2 3 4 │
  │ 5 6 7 8 │
  └         ┘

Display Alternate
[1, 2, 3, 4; 5, 6, 7, 8]

Debug
Matrix { data: [1.0, 2.0, 3.0, 4.0; 5.0, 6.0, 7.0, 8.0], nrows: 2, ncols: 4 }

Debug Alternate
Matrix { 
    data: [1.0, 2.0, 3.0, 4.0; 
           5.0, 6.0, 7.0, 8.0], 
    nrows: 2, 
    ncols: 4 
}

==========================
ArrayStorage
==========================
Debug (derived -> column-major)
ArrayStorage([[1.0, 5.0], [2.0, 6.0], [3.0, 7.0], [4.0, 8.0]])

Display not implemented.

==========================
VecStorage
==========================
Debug (derived -> column-major)
VecStorage { data: [1.0, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0], nrows: Dynamic { value: 2 }, ncols: Dynamic { value: 4 } }

Display not implemented.

Old format: static/dynamic matrices, ArrayStorage and VecStorage
==========================
Static Matrix
==========================
Display

  ┌         ┐
  │ 1 2 3 4 │
  │ 5 6 7 8 │
  └         ┘



Display Alternate

  ┌         ┐
  │ 1 2 3 4 │
  │ 5 6 7 8 │
  └         ┘



Debug
[[1.0, 5.0], [2.0, 6.0], [3.0, 7.0], [4.0, 8.0]]

Alternate
[
    [
        1.0,
        5.0,
    ],
    [
        2.0,
        6.0,
    ],
    [
        3.0,
        7.0,
    ],
    [
        4.0,
        8.0,
    ],
]

==========================
Dynamic Matrix
==========================
Display

  ┌         ┐
  │ 1 2 3 4 │
  │ 5 6 7 8 │
  └         ┘



Display Alternate

  ┌         ┐
  │ 1 2 3 4 │
  │ 5 6 7 8 │
  └         ┘



Debug
VecStorage { data: [1.0, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0], nrows: Dynamic { value: 2 }, ncols: Dynamic { value: 4 } }

Debug Alternate
VecStorage {
    data: [
        1.0,
        5.0,
        2.0,
        6.0,
        3.0,
        7.0,
        4.0,
        8.0,
    ],
    nrows: Dynamic {
        value: 2,
    },
    ncols: Dynamic {
        value: 4,
    },
}

==========================
ArrayStorage
==========================
Debug (derived -> column-major)
[[1.0, 5.0], [2.0, 6.0], [3.0, 7.0], [4.0, 8.0]]

Display not implemented.

==========================
VecStorage
==========================
Debug (derived -> column-major)
VecStorage { data: [1.0, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0], nrows: Dynamic { value: 2 }, ncols: Dynamic { value: 4 } }

Display not implemented.

@Ralith
Copy link
Collaborator

Ralith commented Jun 15, 2022

This Debug formatting is a major regression for the common case. This is important due to e.g. dbg. Please use the single-line semicolon-delimited form, and omit type names for Matrix and single-element wrappers thereof. See also precedent in Vec, as discussed previously.

@Ralith
Copy link
Collaborator

Ralith commented Jun 15, 2022

Perhaps the strongest argument in favor of a readable, concise Debug implementation is the widespread use of derive(Debug) in downstream code, where concise formatting on nalgebra's part can be make-or-break for an aggergate value being usable at all.

@owenbrooks
Copy link
Author

Yep that makes a lot of sense. Thanks for your explanation! How is this:

==========================
Display

  ┌         ┐
  │ 1 2 3 4 │
  │ 5 6 7 8 │
  └         ┘

Display Alternate
[1, 2, 3, 4; 5, 6, 7, 8]

Debug
[1.0, 2.0, 3.0, 4.0; 5.0, 6.0, 7.0, 8.0]

Debug Alternate

[1.0, 2.0, 3.0, 4.0; 
 5.0, 6.0, 7.0, 8.0]

dbg!(dmat)
[src/bin/matrix_print.rs:24] dmat = 
[1.0, 2.0, 3.0, 4.0; 
 5.0, 6.0, 7.0, 8.0]

@Ralith
Copy link
Collaborator

Ralith commented Jun 15, 2022

Thanks, I'm much happier with that (and I appreciate your patience working through all these drafts!). I like what you did with the Debug Alternate format too; that's a nice middle ground, and consistent with the spirit of how it's used in general.

I am a bit confused as to why the default precision is different between Debug and Display. Is that consistent with the impls for f32? If not, should we really be behaving differently?

@owenbrooks
Copy link
Author

owenbrooks commented Jun 15, 2022

Thanks, I'm much happier with that (and I appreciate your patience working through all these drafts!). I like what you did with the Debug Alternate format too; that's a nice middle ground, and consistent with the spirit of how it's used in general.

I am a bit confused as to why the default precision is different between Debug and Display. Is that consistent with the impls for f32? If not, should we really be behaving differently?

Yep, it's just passing through all the formatting options to the underlying float formatter. My understanding is that for floating point Display, when precision is not specified, it tries to create the shortest possible string, omitting the decimal for values like 4.00. It's similar for Debug but the minimum precision is set to 1. See https://doc.rust-lang.org/src/core/fmt/float.rs.html#187

Hmm, I've just realised that this does mean that not everything will be perfectly aligned in debug alternate. My feeling is that is acceptable since Display is handling this. E.g.

Display

  ┌                         ┐
  │ 1.243     2     3     4 │
  │     5     6     7     8 │
  └                         ┘

Display Alternate
[1.243, 2, 3, 4; 5, 6, 7, 8]

Debug
[1.243, 2.0, 3.0, 4.0; 5.0, 6.0, 7.0, 8.0]

Debug Alternate

[1.243, 2.0, 3.0, 4.0; 
 5.0, 6.0, 7.0, 8.0]

[src/bin/matrix_print.rs:24] dmat = 
[1.243, 2.0, 3.0, 4.0; 
 5.0, 6.0, 7.0, 8.0]

@owenbrooks
Copy link
Author

New output
==================
Point
==================
Normal

  ┌            ┐
  │ 1.12345678 │
  │ 2.12345678 │
  │ 3.12345678 │
  └            ┘

Alternate
[1.12345678, 2.12345678, 3.12345678]

Debug
[1.12345678, 2.12345678, 3.12345678]

Debug alternate
[
    1.12345678,
    2.12345678,
    3.12345678,
]

==================
Scale
==================
Normal

  ┌   ┐
  │ 1 │
  │ 2 │
  └   ┘

Alternate
[1, 2]

Debug
[1.0, 2.0]

Debug alternate
[
    1.0,
    2.0,
]

==================
Translation
==================
Normal

  ┌   ┐
  │ 1 │
  │ 2 │
  │ 3 │
  └   ┘

Alternate
[1, 2, 3]

Debug
[1.0, 2.0, 3.0]

Debug alternate
[
    1.0,
    2.0,
    3.0,
]

==================
Matrix
==================
Normal

  ┌         ┐
  │ 1.2 2.4 │
  │ 3.1 4.5 │
  └         ┘

Alternate
[1.2, 2.4; 3.1, 4.5]

Debug
[1.2, 2.4; 3.1, 4.5]

Debug alternate

[1.2, 2.4; 
 3.1, 4.5]

==================
UnitQuaternion
==================
Normal
{ angle: 3.141592653589793, axis: (0, 1, 0) }

Alternate
{ angle: 3.141592653589793, axis: (0, 1, 0) }

Debug
Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 6.123233995736766e-17 }

Debug alternate
Quaternion {
    x: 0.0,
    y: 1.0,
    z: 0.0,
    w: 6.123233995736766e-17,
}

==================
Isometry
==================
Normal
{ translation: [1, 2, 3], rotation: { angle: 3.141592653589793, axis: (0, 1, 0) } }

Debug
Isometry { rotation: Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 6.123233995736766e-17 }, translation: [1.0, 2.0, 3.0] }

Debug alternate
Isometry {
    rotation: Quaternion {
        x: 0.0,
        y: 1.0,
        z: 0.0,
        w: 6.123233995736766e-17,
    },
    translation: [
        1.0,
        2.0,
        3.0,
    ],
}

==================
UnitComplex
==================
Normal
UnitComplex angle: 0.15

Debug
Complex { re: 0.9887710779360422, im: 0.14943813247359922 }

Debug alternate
Complex {
    re: 0.9887710779360422,
    im: 0.14943813247359922,
}

==================
Similarity
==================
Normal
{ translation: [1, 2, 3], rotation: { angle: 3.141592653589793, axis: (0, 1, 0) }, scaling: 0.1 }

Debug
Similarity { isometry: Isometry { rotation: Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 6.123233995736766e-17 }, translation: [1.0, 2.0, 3.0] }, scaling: 0.1 }

Debug alternate
Similarity {
    isometry: Isometry {
        rotation: Quaternion {
            x: 0.0,
            y: 1.0,
            z: 0.0,
            w: 6.123233995736766e-17,
        },
        translation: [
            1.0,
            2.0,
            3.0,
        ],
    },
    scaling: 0.1,
}

==================
Quaternion
==================
Normal
2 + 3i + 4j + 1k

Debug
Quaternion { x: 2.0, y: 3.0, z: 4.0, w: 1.0 }

Debug alternate
Quaternion {
    x: 2.0,
    y: 3.0,
    z: 4.0,
    w: 1.0,
}

==================
UnitDualQuaternion
==================
Normal
{ translation: [0, 0, 0], rotation: { angle: 3.141592653589793, axis: (0, 1, 0) } }

Debug
DualQuaternion { real: Quaternion { x: 0.0, y: 1.0, z: 0.0, w: 6.123233995736766e-17 }, dual: Quaternion { x: 0.0, y: 0.0, z: 0.0, w: 0.0 } }

Debug alternate
DualQuaternion {
    real: Quaternion {
        x: 0.0,
        y: 1.0,
        z: 0.0,
        w: 6.123233995736766e-17,
    },
    dual: Quaternion {
        x: 0.0,
        y: 0.0,
        z: 0.0,
        w: 0.0,
    },
}

@Ralith
Copy link
Collaborator

Ralith commented Jun 15, 2022

I've just realised that this does mean that not everything will be perfectly aligned in debug alternate

I think this is fine. People who care can use padding/precision specifiers as usual.

New output

I still personally prefer the more concise Debug form for Quaternion had by delegating directly to the inner coords. If we don't feel it necessary to name the fields in Vector4, I'm not sure why we'd go out of our way to do so for Quaternion.

@Andlon
Copy link
Sponsor Collaborator

Andlon commented Jun 15, 2022

Just a few comments (have more to say but no time atm):

I do think alignment in alternate debug mode matters a lot for readability, but I think this can be punted to a separate PR. It's any way a huge improvement over the current (very broken!) state.

I still personally prefer the more concise Debug form for Quaternion had by delegating directly to the inner coords. If we don't feel it necessary to name the fields in Vector4, I'm not sure why we'd go out of our way to do so for Quaternion.

For a vector, the order of the components is given. A quaternion has multiple ways in which it can be represented as a vector, say, as [x, y, z, w] or [w, x, y, z]. So I think just 4 numbers for a quaternion is ultimately ambiguous. I would personally prefer this be made more explicit.

@Ralith
Copy link
Collaborator

Ralith commented Jun 15, 2022

I think just 4 numbers for a quaternion is ultimately ambiguous

That's a fair point, although the documentation is clear. How about ([x, y, z], w)? Maybe that's too distant from construction...

My opinion here isn't super strong, this PR is a nice improvement as-is.

Copy link
Author

@owenbrooks owenbrooks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would appreciate feedback on these parts

Comment on lines 206 to 225
let nrows = self.nrows();
let ncols = self.ncols();
for i in 0..nrows {
for j in 0..ncols {
if j != 0 {
write!(f, ", ")?;
}
// Safety: the indices are within range
unsafe {
(*self.data.get_unchecked(i, j)).fmt(f)?;
}
}
if i != nrows - 1 {
write!(f, "; ")?;
}
if f.alternate() && i != nrows - 1 {
write!(f, "\n ")?;
}
}
write!(f, "]")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a more straightforward / nicer way of looping through in this order?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but I wouldn't overthink it; this seems fine.

Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine: it's clear what's going on. Is the unsafe really necessary though? It seems to me that the bounds checks here won't lead to a very significant performance penalty given the cost of formatting etc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I've replaced it with self[(i, j)].fmt(f)?;

Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized that the original code (with unsafe) is in fact unsound for 0-row matrices. It will still panic with your replacement. It's a bit tricky to get this right. Here's a quick attempt I made, which I think should work in all cases?

(btw, would be great to have some unit tests that also cover the 0-dimension corner cases)

        if f.alternate() {
            writeln!(f, "")?;
        }
        write!(f, "[")?;
        
        let row_separator = match f.alternate() {
            true => ";\n ",
            false => "; ",
        };
        
        for (i, row) in self.row_iter().enumerate() {
            if i > 0 {
                write!(f, "{row_separator}")?;
            }
            for (j, element) in row.iter().enumerate() {
                if j > 0 {
                    write!(f, ", ")?;
                }
                element.fmt(f)?;
            }
        }
        write!(f, "]")

Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and also be wary of the overflow for evaluating nrows - 1 if nrows == 0!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, wouldn't the for loops only run the code if the dimensions are non-zero? for i in 0..0 never runs for instance. Good catch on nrows - 1

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That code looks good to me, I've added it in.

Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, wouldn't the for loops only run the code if the dimensions are non-zero? for i in 0..0 never runs for instance. Good catch on nrows - 1

Yeah, you're right about that! Didn't think that through :-)

Comment on lines 1984 to 2001
/// Displays a vector using commas as the delimiter
pub fn format_column_vec_as_row<T: Scalar + fmt::Display, D: crate::DimName>(
vector: &OVector<T, D>,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result
where
DefaultAllocator: Allocator<T, D>,
{
write!(f, "[")?;
let mut it = vector.iter();
std::fmt::Display::fmt(it.next().unwrap(), f)?;
for comp in it {
write!(f, ", ")?;
std::fmt::Display::fmt(comp, f)?;
}
write!(f, "]")
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure where this function should go. It is used for the vector-wrapping geometry types.

Comment on lines 1913 to 1945
writeln!(
f,
" └ {:>width$} ┘",
"",
width = max_length_with_space * ncols - 1
)?;
writeln!(f)
write!(
f,
" └ {:>width$} ┘",
"",
width = max_length_with_space * ncols - 1
)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously this added two newlines after the end of the matrix. i.e. [ 1.0 ]/n/n
This removes both of those giving flexibility to write immediately after the matrix. Could add one or both of the newlines back in if we don't want to change it too much.

println!("Display");
println!("{}directly after", smat);
println!("next line");

Old:

Display

  ┌         ┐
  │ 1 2 3 4 │
  │ 5 6 7 8 │
  └         ┘

directly after
next line

New:

Display

  ┌         ┐
  │ 1 2 3 4 │
  │ 5 6 7 8 │
  └         ┘directly after
next line

Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like the right decision to me.

Is the initial newline necessary though? I guess it is in order to give reasonable formatting for cases where you have e.g.

println!("My matrix is {matrix}");

I guess if you don't have a leading newline, cases like this will end up with messed up formatting?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I assume that is why the initial newline is included. I wouldn't be against removing it myself.

{
write!(f, "[")?;
let mut it = vector.iter();
std::fmt::Display::fmt(it.next().unwrap(), f)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice not to panic on 0-element vectors, unless we're really utterly certain that will never occur (but the signature doesn't suggest as much).

Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0-length vectors can easily occur in practice, so this is important!

Would be nice to have some unit tests that include 0-element vectors maybe..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a test for 0-element matrices, though I'm not sure how to add a test for this function directly. Since calling that function requires a Formatter struct, and I don't think I can construct a 0-length translation/point to test with.

{
write!(f, "[")?;
let mut it = vector.iter();
std::fmt::Display::fmt(it.next().unwrap(), f)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we're fully-qualifying fmt everywhere rather than just invoking it as a method?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was to disambiguate it from std::fmt::Debug::fmt.

@Andlon
Copy link
Sponsor Collaborator

Andlon commented Jun 17, 2022

I think just 4 numbers for a quaternion is ultimately ambiguous

That's a fair point, although the documentation is clear. How about ([x, y, z], w)? Maybe that's too distant from construction...

My opinion here isn't super strong, this PR is a nice improvement as-is.

Hmm, you're right that the docs are pretty clear on the storage order. I generally agree with you that conciseness is really important, given that during debugging you might be outputting complex structs or large amounts of data, and verbose output is a real hindrance to effective debugging in my experience. So I think I'd also be in favor of the more compact 4-number output for quaternions.

Comment on lines +1990 to +2001
if vector.is_empty() {
return write!(f, "[ ]");
}

write!(f, "[")?;
let mut it = vector.iter();
std::fmt::Display::fmt(it.next().unwrap(), f)?;
for comp in it {
write!(f, ", ")?;
std::fmt::Display::fmt(comp, f)?;
}
write!(f, "]")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this could be simpler/less unwrappy:

Suggested change
if vector.is_empty() {
return write!(f, "[ ]");
}
write!(f, "[")?;
let mut it = vector.iter();
std::fmt::Display::fmt(it.next().unwrap(), f)?;
for comp in it {
write!(f, ", ")?;
std::fmt::Display::fmt(comp, f)?;
}
write!(f, "]")
write!(f, "[")?;
let mut it = vector.iter();
if let Some(first) = it.next() {
std::fmt::Display::fmt(first, f)?;
for comp in it {
write!(f, ", ")?;
std::fmt::Display::fmt(comp, f)?;
}
}
write!(f, "]")

This also avoids the whitespace in empty vectors, which I think is more consistent.

@Andlon
Copy link
Sponsor Collaborator

Andlon commented Sep 5, 2022

@owenbrooks: do you intend to continue working on this issue? I think your contribution here is very valuable and I think we were close to having converged. Would be nice to wrap it up if we can, although of course I understand if other things take priority for you.

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.

None yet

4 participants