Repository: idaholab/MontePy (language: python)

Issue:
Trailing comments aren't being properly transferred
While working on #351 I noticed that `cell.comments` was wrong. 

The following cell:

``` 
100103  3926  5.00000E-02 (-102002          -36425  36426):   $ Lower Rodlet Sodium Bond
                          (-102002  169239  -36426  36427)    u=424 
c
c comment for next cell... 
c
... next cell
```

Had too many comments in `cell.comments`

``` python
In [13]: cell.comments
Out[13]:
[COMMENT:
 $ Lower Rodlet Sodium Bond,
 COMMENT:
 c,
 COMMENT:
 c comment for next cell... 
 COMMENT:
 c,
```


**Additional context**
Version: `0.3.0dev1`

Note to self: see if next cell actually got parsed

Extra blank lines are sometimes added on write_problem
<!--
**Reminders**
1. This is not a place to debug your MCNP models
1. MCNP is export controlled and only its 6.2 and 6.3 user manuals are public. Don't include any information about MCNP which is not in those manuals.
1. Your model may be export controlled, or proprietary. Please change specific numbers (e.g., dimensions, material compositions, etc.) in your minimum working examples. -->

**Describe the bug**

This came from #520 testing, and specifically `tests/inputs/..`: `test_universe.imcnp`, `test_interp_edge.imcnp`, `testReadEdgeCase.imcnp`.

**To Reproduce**

A short code snippet of what you have ran. Please change or remove any specific values or anything that can't be public. For example:

``` python
montepy.read_input("foo.imcnp").write_problem('bar.imcnp')
```



**MCNP input file snippet**

If applicable, please include a small section of the input file you were working on. If it includes any specific values please change or remove them. For example:

```
MCNP Test Model for MOAA
C cells
c
1 1 20
         -1000  $ dollar comment
     imp:n,p=1 trcl=5
     u=1
2 2 8
      -1005
     imp:n=1
     imp:p=0.5
     lat 1 
     fill= 0:1 0:1 0:0 1 0 0 1 (5)
3 3 -1
      1000 1005 -1010
     imp:n,p=1
99 0
      1010
     imp:n,p=0
     fill=1
5 0 
      #99
      imp:n,p=3 fill=1 (1 0.0 0.0)
c foo end comment

C surfaces
1000 SO 1
1005 RCC 0 1.5 -0.5 0 0 1 0.25
1010 SO 3

C data
C materials
C UO2 5 atpt enriched
m1        92235.80c           5 &
92238.80c          95
c testing comments
sc1 This is a high quality source comment
Fc5 Such a good tally comment.
C Iron
m2        26054.80c        5.85
          26056.80c       91.75
          26057.80c        2.12
          26058.80c        0.28
C water
m3        1001.80c           2
           8016.80c           1
MT3 lwtr.23t
TR5 0.0 0.0 1.0
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
phys:p j 1 2j 1
mode n p
vol NO 2J 1 1.5 J
```

This writes out as:

``` 
MCNP Test Model for MOAA
C cells
c
1 1 20
         -1000  $ dollar comment
     IMP:n=0.0
2 2 8
      -1005
     imp:n=1
     imp:p=0.5 $ graveyard
3 3 -1
      1000 1005 -1010
c
c foo end comment
c
     IMP:n=0.0
99 0 1000:-1005 1010 $graveyard
     IMP:n=0.0
5 0
      #99 IMP:n=0.0

C surfaces
1000 SO 1
1005 RCC 0 1.5 -0.5 0 0 1 0.25
1010 SO 3

C data
C materials
C UO2 5 atpt enriched
u 350 2J -1
FILL 2J 350
m1        92235.80c           5 &
92238.80c          95
C Iron

m2        26054.80c        5.85
          26056.80c       91.75
          26057.80c        2.12
          26058.80c        0.28
C water
m3        1001.80c           2
           8016.80c           1
MT3 lwtr.23t
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
phys:p j 1 2j 1
mode n p
vol NO 2J 1 1.5 J
```

**Version**

 - Version 0.4.0


**Additional context**

Add any other context about the problem here.

Test writing all valid MCNP input files out
With #493, writing to file is a lot less resource intensive. We should test all input files for similar issues to #172 
Extra blank lines are sometimes added on write_problem
<!--
**Reminders**
1. This is not a place to debug your MCNP models
1. MCNP is export controlled and only its 6.2 and 6.3 user manuals are public. Don't include any information about MCNP which is not in those manuals.
1. Your model may be export controlled, or proprietary. Please change specific numbers (e.g., dimensions, material compositions, etc.) in your minimum working examples. -->

**Describe the bug**

This came from #520 testing, and specifically `tests/inputs/..`: `test_universe.imcnp`, `test_interp_edge.imcnp`, `testReadEdgeCase.imcnp`.

**To Reproduce**

A short code snippet of what you have ran. Please change or remove any specific values or anything that can't be public. For example:

``` python
montepy.read_input("foo.imcnp").write_problem('bar.imcnp')
```



**MCNP input file snippet**

If applicable, please include a small section of the input file you were working on. If it includes any specific values please change or remove them. For example:

```
MCNP Test Model for MOAA
C cells
c
1 1 20
         -1000  $ dollar comment
     imp:n,p=1 trcl=5
     u=1
2 2 8
      -1005
     imp:n=1
     imp:p=0.5
     lat 1 
     fill= 0:1 0:1 0:0 1 0 0 1 (5)
3 3 -1
      1000 1005 -1010
     imp:n,p=1
99 0
      1010
     imp:n,p=0
     fill=1
5 0 
      #99
      imp:n,p=3 fill=1 (1 0.0 0.0)
c foo end comment

C surfaces
1000 SO 1
1005 RCC 0 1.5 -0.5 0 0 1 0.25
1010 SO 3

C data
C materials
C UO2 5 atpt enriched
m1        92235.80c           5 &
92238.80c          95
c testing comments
sc1 This is a high quality source comment
Fc5 Such a good tally comment.
C Iron
m2        26054.80c        5.85
          26056.80c       91.75
          26057.80c        2.12
          26058.80c        0.28
C water
m3        1001.80c           2
           8016.80c           1
MT3 lwtr.23t
TR5 0.0 0.0 1.0
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
phys:p j 1 2j 1
mode n p
vol NO 2J 1 1.5 J
```

This writes out as:

``` 
MCNP Test Model for MOAA
C cells
c
1 1 20
         -1000  $ dollar comment
     IMP:n=0.0
2 2 8
      -1005
     imp:n=1
     imp:p=0.5 $ graveyard
3 3 -1
      1000 1005 -1010
c
c foo end comment
c
     IMP:n=0.0
99 0 1000:-1005 1010 $graveyard
     IMP:n=0.0
5 0
      #99 IMP:n=0.0

C surfaces
1000 SO 1
1005 RCC 0 1.5 -0.5 0 0 1 0.25
1010 SO 3

C data
C materials
C UO2 5 atpt enriched
u 350 2J -1
FILL 2J 350
m1        92235.80c           5 &
92238.80c          95
C Iron

m2        26054.80c        5.85
          26056.80c       91.75
          26057.80c        2.12
          26058.80c        0.28
C water
m3        1001.80c           2
           8016.80c           1
MT3 lwtr.23t
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
phys:p j 1 2j 1
mode n p
vol NO 2J 1 1.5 J
```

**Version**

 - Version 0.4.0


**Additional context**

Add any other context about the problem here.

Trailing comments aren't being properly transferred
While working on #351 I noticed that `cell.comments` was wrong. 

The following cell:

``` 
100103  3926  5.00000E-02 (-102002          -36425  36426):   $ Lower Rodlet Sodium Bond
                          (-102002  169239  -36426  36427)    u=424 
c
c comment for next cell... 
c
... next cell
```

Had too many comments in `cell.comments`

``` python
In [13]: cell.comments
Out[13]:
[COMMENT:
 $ Lower Rodlet Sodium Bond,
 COMMENT:
 c,
 COMMENT:
 c comment for next cell... 
 COMMENT:
 c,
```


**Additional context**
Version: `0.3.0dev1`

Note to self: see if next cell actually got parsed

Trailing comments aren't being properly transferred
While working on #351 I noticed that `cell.comments` was wrong. 

The following cell:

``` 
100103  3926  5.00000E-02 (-102002          -36425  36426):   $ Lower Rodlet Sodium Bond
                          (-102002  169239  -36426  36427)    u=424 
c
c comment for next cell... 
c
... next cell
```

Had too many comments in `cell.comments`

``` python
In [13]: cell.comments
Out[13]:
[COMMENT:
 $ Lower Rodlet Sodium Bond,
 COMMENT:
 c,
 COMMENT:
 c comment for next cell... 
 COMMENT:
 c,
```


**Additional context**
Version: `0.3.0dev1`

Note to self: see if next cell actually got parsed

Extra blank lines are sometimes added on write_problem
<!--
**Reminders**
1. This is not a place to debug your MCNP models
1. MCNP is export controlled and only its 6.2 and 6.3 user manuals are public. Don't include any information about MCNP which is not in those manuals.
1. Your model may be export controlled, or proprietary. Please change specific numbers (e.g., dimensions, material compositions, etc.) in your minimum working examples. -->

**Describe the bug**

This came from #520 testing, and specifically `tests/inputs/..`: `test_universe.imcnp`, `test_interp_edge.imcnp`, `testReadEdgeCase.imcnp`.

**To Reproduce**

A short code snippet of what you have ran. Please change or remove any specific values or anything that can't be public. For example:

``` python
montepy.read_input("foo.imcnp").write_problem('bar.imcnp')
```



**MCNP input file snippet**

If applicable, please include a small section of the input file you were working on. If it includes any specific values please change or remove them. For example:

```
MCNP Test Model for MOAA
C cells
c
1 1 20
         -1000  $ dollar comment
     imp:n,p=1 trcl=5
     u=1
2 2 8
      -1005
     imp:n=1
     imp:p=0.5
     lat 1 
     fill= 0:1 0:1 0:0 1 0 0 1 (5)
3 3 -1
      1000 1005 -1010
     imp:n,p=1
99 0
      1010
     imp:n,p=0
     fill=1
5 0 
      #99
      imp:n,p=3 fill=1 (1 0.0 0.0)
c foo end comment

C surfaces
1000 SO 1
1005 RCC 0 1.5 -0.5 0 0 1 0.25
1010 SO 3

C data
C materials
C UO2 5 atpt enriched
m1        92235.80c           5 &
92238.80c          95
c testing comments
sc1 This is a high quality source comment
Fc5 Such a good tally comment.
C Iron
m2        26054.80c        5.85
          26056.80c       91.75
          26057.80c        2.12
          26058.80c        0.28
C water
m3        1001.80c           2
           8016.80c           1
MT3 lwtr.23t
TR5 0.0 0.0 1.0
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
phys:p j 1 2j 1
mode n p
vol NO 2J 1 1.5 J
```

This writes out as:

``` 
MCNP Test Model for MOAA
C cells
c
1 1 20
         -1000  $ dollar comment
     IMP:n=0.0
2 2 8
      -1005
     imp:n=1
     imp:p=0.5 $ graveyard
3 3 -1
      1000 1005 -1010
c
c foo end comment
c
     IMP:n=0.0
99 0 1000:-1005 1010 $graveyard
     IMP:n=0.0
5 0
      #99 IMP:n=0.0

C surfaces
1000 SO 1
1005 RCC 0 1.5 -0.5 0 0 1 0.25
1010 SO 3

C data
C materials
C UO2 5 atpt enriched
u 350 2J -1
FILL 2J 350
m1        92235.80c           5 &
92238.80c          95
C Iron

m2        26054.80c        5.85
          26056.80c       91.75
          26057.80c        2.12
          26058.80c        0.28
C water
m3        1001.80c           2
           8016.80c           1
MT3 lwtr.23t
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
phys:p j 1 2j 1
mode n p
vol NO 2J 1 1.5 J
```

**Version**

 - Version 0.4.0


**Additional context**

Add any other context about the problem here.

Trailing comments aren't being properly transferred
While working on #351 I noticed that `cell.comments` was wrong. 

The following cell:

``` 
100103  3926  5.00000E-02 (-102002          -36425  36426):   $ Lower Rodlet Sodium Bond
                          (-102002  169239  -36426  36427)    u=424 
c
c comment for next cell... 
c
... next cell
```

Had too many comments in `cell.comments`

``` python
In [13]: cell.comments
Out[13]:
[COMMENT:
 $ Lower Rodlet Sodium Bond,
 COMMENT:
 c,
 COMMENT:
 c comment for next cell... 
 COMMENT:
 c,
```


**Additional context**
Version: `0.3.0dev1`

Note to self: see if next cell actually got parsed

Extra blank lines are sometimes added on write_problem
<!--
**Reminders**
1. This is not a place to debug your MCNP models
1. MCNP is export controlled and only its 6.2 and 6.3 user manuals are public. Don't include any information about MCNP which is not in those manuals.
1. Your model may be export controlled, or proprietary. Please change specific numbers (e.g., dimensions, material compositions, etc.) in your minimum working examples. -->

**Describe the bug**

This came from #520 testing, and specifically `tests/inputs/..`: `test_universe.imcnp`, `test_interp_edge.imcnp`, `testReadEdgeCase.imcnp`.

**To Reproduce**

A short code snippet of what you have ran. Please change or remove any specific values or anything that can't be public. For example:

``` python
montepy.read_input("foo.imcnp").write_problem('bar.imcnp')
```



**MCNP input file snippet**

If applicable, please include a small section of the input file you were working on. If it includes any specific values please change or remove them. For example:

```
MCNP Test Model for MOAA
C cells
c
1 1 20
         -1000  $ dollar comment
     imp:n,p=1 trcl=5
     u=1
2 2 8
      -1005
     imp:n=1
     imp:p=0.5
     lat 1 
     fill= 0:1 0:1 0:0 1 0 0 1 (5)
3 3 -1
      1000 1005 -1010
     imp:n,p=1
99 0
      1010
     imp:n,p=0
     fill=1
5 0 
      #99
      imp:n,p=3 fill=1 (1 0.0 0.0)
c foo end comment

C surfaces
1000 SO 1
1005 RCC 0 1.5 -0.5 0 0 1 0.25
1010 SO 3

C data
C materials
C UO2 5 atpt enriched
m1        92235.80c           5 &
92238.80c          95
c testing comments
sc1 This is a high quality source comment
Fc5 Such a good tally comment.
C Iron
m2        26054.80c        5.85
          26056.80c       91.75
          26057.80c        2.12
          26058.80c        0.28
C water
m3        1001.80c           2
           8016.80c           1
MT3 lwtr.23t
TR5 0.0 0.0 1.0
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
phys:p j 1 2j 1
mode n p
vol NO 2J 1 1.5 J
```

This writes out as:

``` 
MCNP Test Model for MOAA
C cells
c
1 1 20
         -1000  $ dollar comment
     IMP:n=0.0
2 2 8
      -1005
     imp:n=1
     imp:p=0.5 $ graveyard
3 3 -1
      1000 1005 -1010
c
c foo end comment
c
     IMP:n=0.0
99 0 1000:-1005 1010 $graveyard
     IMP:n=0.0
5 0
      #99 IMP:n=0.0

C surfaces
1000 SO 1
1005 RCC 0 1.5 -0.5 0 0 1 0.25
1010 SO 3

C data
C materials
C UO2 5 atpt enriched
u 350 2J -1
FILL 2J 350
m1        92235.80c           5 &
92238.80c          95
C Iron

m2        26054.80c        5.85
          26056.80c       91.75
          26057.80c        2.12
          26058.80c        0.28
C water
m3        1001.80c           2
           8016.80c           1
MT3 lwtr.23t
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
phys:p j 1 2j 1
mode n p
vol NO 2J 1 1.5 J
```

**Version**

 - Version 0.4.0


**Additional context**

Add any other context about the problem here.

Trailing comments aren't being properly transferred
While working on #351 I noticed that `cell.comments` was wrong. 

The following cell:

``` 
100103  3926  5.00000E-02 (-102002          -36425  36426):   $ Lower Rodlet Sodium Bond
                          (-102002  169239  -36426  36427)    u=424 
c
c comment for next cell... 
c
... next cell
```

Had too many comments in `cell.comments`

``` python
In [13]: cell.comments
Out[13]:
[COMMENT:
 $ Lower Rodlet Sodium Bond,
 COMMENT:
 c,
 COMMENT:
 c comment for next cell... 
 COMMENT:
 c,
```


**Additional context**
Version: `0.3.0dev1`

Note to self: see if next cell actually got parsed

Shortcuts recompressed incorrectly
**Describe the bug**

Shortcuts for data entry are understood by MontePy, but they can erroneously "absorb" the first entry.

They also seem to be introducing blank lines where none belong (or maybe that is due to comments).

**To Reproduce**

``` python
import montepy
deck = montepy.read_input("testi.i")
c = deck.cells[4]
print(f"Imp:n={c.importance.neutron}, Imp:p={c.importance.photon}, Vol={c.volume}")
deck.write_problem("testo.i", overwrite=True)
deck = montepy.read_input("testo.i")
c = deck.cells[4]
print(f"Imp:n={c.importance.neutron}, Imp:p={c.importance.photon}, Vol={c.volume}")
```


**MCNP input file snippet: testi.i**


```
MCNP Test Model
C Cells
C Graveyard
99    0 +1010
C Seven spheres within a sphere
1 1 -10 -1001
2 1 -10 -1002
3 1 -10 -1003
4 1 -10 -1004
5 1 -10 -1005
6 1 -10 -1006
7 1 -10 -1007
8     0 -1010  1001  5i  1007

C surfaces
1001 SO      0.1
1002 SX -1.1 0.1
1003 SX +1.1 0.1
1004 SY -1.1 0.1
1005 SY +1.1 0.1
1006 SZ -1.1 0.1
1007 SZ +1.1 0.1
1010 SO 3

C data
C This is what the importance should expand to.
imp:n 0 1 2 3  10 12 14 16 1
C But, linear interpolation recompressed incorrectly
imp:p 0 2i 3   10 2i 16    1
C Repeat is also recompressed incorrectly
VOL   J 1 2R 2 10 2R 20    3
C materials
C UO2 5 atpt enriched
m1        92235.80c           5
          92238.80c          95
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
mode n p

```

**Output**

The cell modifier parameters are set correctly. Here is Cell 4. However, they are written out incorrectly.

``` python
Imp:n=10.0, Imp:p=10.0, Vol=2.0

Traceback (most recent call last):
  File "./badinterp/montepy/mcnp_object.py", line 50, in __init__
    self._tree = parser.parse(input.tokenize(), input)
  File "./badinterp/montepy/input_parser/parser_base.py", line 156, in parse
    tree = super().parse(token_generator)
  File "/opt/Mamba/forge3/envs/cafe/lib/python3.9/site-packages/sly/yacc.py", line 2137, in parse
    value = p.func(self, pslice)
  File "./badinterp/montepy/input_parser/parser_base.py", line 243, in shortcut_sequence
    short_cut = syntax_node.ShortcutNode(p)
  File "./badinterp/montepy/input_parser/syntax_node.py", line 1694, in __init__
    self._expand_repeat(p)
  File "./badinterp/montepy/input_parser/syntax_node.py", line 1758, in _expand_repeat
    raise ValueError(f"Repeat cannot follow a jump. Given: {list(p)}")
ValueError: Repeat cannot follow a jump. Given: [(shortcut:Shortcuts.JUMP: [(Value, None, padding: None)]), '3R']
```

**MCNP input file snippet: testo.i**

```
MCNP Test Model
C Cells
C Graveyard
99    0 +1010
C Seven spheres within a sphere
1 1 -10 -1001 
2 1 -10 -1002 
3 1 -10 -1003 
4 1 -10 -1004 
5 1 -10 -1005 
6 1 -10 -1006 
7 1 -10 -1007 
8     0 -1010  1001  1002  1003  1004  1005  1006  1007 

C surfaces
1001 SO      0.1
1002 SX -1.1 0.1
1003 SX +1.1 0.1
1004 SY -1.1 0.1
1005 SY +1.1 0.1
1006 SZ -1.1 0.1
1007 SZ +1.1 0.1
1010 SO 3

C data
C This is what the importance should expand to.
imp:n 0 1 2 3  10 12 14 16 1
C But, linear interpolation recompressed incorrectly
     
imp:p 0 2i 3   3i 16    1
C Repeat is also recompressed incorrectly
     
C Repeat is also recompressed incorrectly
VOL   J 3R 2 10 2R 20    
C materials
C UO2 5 atpt enriched
m1        92235.80c           5
          92238.80c          95
C execution
ksrc 0 0 0
kcode 100000 1.000 50 1050
mode n p



```


**Version**

 - Versions 0.2.10 - 0.3.3 ([0ade822](https://github.com/idaholab/MontePy/commit/0ade8227cc288cb53481dd435a5a211edadc72f7))


**Additional context**

See also #489.

Fix issue with shortcut recompression
<!--
If you are a first-time contributor to MontePy,
refer the developing guidelines at:
https://idaholab.github.io/MontePy/developing.html
-->

# Description

## Bug fix #499
This fixes the bug from #499. This bug came down to a bug when two repeat shortcuts are touching. The second repeat assumes that the repeat node to its left is the value this one is repeating. To solve this the logic is updated to see if these edge values match.

## Test Improvement #497 
In the process I implemented testing where all valid input files are readin, "written" out and then re-read in, and the original and new files are compared. This found various padding bugs. 

## Bug fix: #489
For the bug: #489, interpolations (and all shortcuts) in geometry definitions were not recompressed. This was due to the fact that the `GeometryTree` had no idea that a shortcut was involved. This will be fixed by:

1. Making it possible to have an interpolation of integer values only.
2. Labeling `GeometryTree` accordingly when it comes from a shortcut
3. Creating a method to recompress a shortcut in a `GeoemtryTree`. 

## Bug Fix #526 

This is actually #352 in disguise. 

## Bug fix #352

This was due to the trailing comments not being found properly due to a few reasons. In #352 the issue was due to the default trees for cell modifiers being added. This makes it look like there were no trailing comments.

This case was fixed by flagging default tree items in the parameters, and ignoring them for transferring comments.

The other case found was not being transferred from a tally comment. In this case the padding was attached to the classifier. The `get_trailing_comment` logic had to be updated to allow falling through to that very first node in the tree. This issue was in part causing #526 due to the new line additions due to the comment being in the wrong object.

- [x] Fixes #499 
- [x] Fixes #519 
- [x] Fixes #497 
- [x] fixes #489
- [x] fixes #526 
- [x] Fixes #352.

# Checklist

- [x] I have performed a self-review of my own code
- [x] I have made corresponding changes to the documentation (if applicable)
- [x] I have added tests that prove my fix is effective or that my feature works (if applicable)
- [x] Update unit testing to be more thurough
- [x] Cover logic case of `Jump` `Repeat`
- [x] Implement more unit tests for geometry shortcuts. 
<!--
While tests will automatically be checked by CI, it is good practice to
ensure that they pass locally first. 
-->



Discussion / hints:
@tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases. 
@MicahGale test this.
> @tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases.

I didn't post it here, but I was able to reproduce the same behavior with 0.3.3: ~~cards~~ inputs ate up the following comments, leading to some non-breaking but awkward behavior (see #484).
Did it lead to anything not parsing?

If not I'm thinking of calling this "low priority". 

From #493 we should also work on setting up global test fixtures for this test suite now. 

@tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases. 
@MicahGale test this.
> @tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases.

I didn't post it here, but I was able to reproduce the same behavior with 0.3.3: ~~cards~~ inputs ate up the following comments, leading to some non-breaking but awkward behavior (see #484).
Did it lead to anything not parsing?

If not I'm thinking of calling this "low priority". 
@tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases. 
@MicahGale test this.
> @tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases.

I didn't post it here, but I was able to reproduce the same behavior with 0.3.3: ~~cards~~ inputs ate up the following comments, leading to some non-breaking but awkward behavior (see #484).
Did it lead to anything not parsing?

If not I'm thinking of calling this "low priority". 

@tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases. 
@MicahGale test this.
> @tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases.

I didn't post it here, but I was able to reproduce the same behavior with 0.3.3: ~~cards~~ inputs ate up the following comments, leading to some non-breaking but awkward behavior (see #484).
Did it lead to anything not parsing?

If not I'm thinking of calling this "low priority". 

@tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases. 
@MicahGale test this.
> @tjlaboss could you test this out on 0.3.2? I feel like this should be resolved by one of the recent releases.

I didn't post it here, but I was able to reproduce the same behavior with 0.3.3: ~~cards~~ inputs ate up the following comments, leading to some non-breaking but awkward behavior (see #484).
Did it lead to anything not parsing?

If not I'm thinking of calling this "low priority". 



Fix patch:
diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst
index 68171836..1c708263 100644
--- a/doc/source/changelog.rst
+++ b/doc/source/changelog.rst
@@ -4,10 +4,17 @@ MontePy Changelog
 #Next Version#
 --------------
 
+**Features Added**
+
+* Added support for reading an input from either file paths or streams (file handles) with ``montepy.read_input`` (:issue:`519`).
+
 **Bug Fixes**
 
 * Fixed a bug where ``problem.materials.append_renumber`` would double add a material to ``problem.data_inputs`` (:issue:`516`).
 * Fixed bug where material-level library specifications (e.g., ``m1 plib=84p``) could not be fully parsed (:issue:`521`).
+* Fixed bug with shortcuts right after another shortcut (e.g., ``1 2M 3R``) not being properly recompressed on export (:issue:`499`).
+* Fixed bug with shortcuts in cell geometry definitions not being recompressed on export (:issue:`489`).
+* Fixed bug where leading comments were not always transferred to the appropriate input. (:issue:`352`, :issue:`526`).
 
 **Performance Improvement**
 
diff --git a/montepy/cell.py b/montepy/cell.py
index 98d6ac5a..c2600fd0 100644
--- a/montepy/cell.py
+++ b/montepy/cell.py
@@ -142,7 +142,7 @@ def _parse_keyword_modifiers(self):
                         break
             if (class_pref == "imp" and not has_imp) or class_pref != "imp":
                 tree = getattr(self, attr)._tree
-                self._tree["parameters"].append(tree)
+                self._tree["parameters"].append(tree, True)
 
     def _load_blank_modifiers(self):
         """
diff --git a/montepy/data_inputs/importance.py b/montepy/data_inputs/importance.py
index 8ef1c322..6c8b38cf 100644
--- a/montepy/data_inputs/importance.py
+++ b/montepy/data_inputs/importance.py
@@ -29,6 +29,7 @@ class Importance(CellModifierInput):
     def __init__(self, input=None, in_cell_block=False, key=None, value=None):
         self._particle_importances = {}
         self._real_tree = {}
+        self._part_combos = []
         super().__init__(input, in_cell_block, key, value)
         if self.in_cell_block:
             if key:
@@ -39,6 +40,7 @@ def __init__(self, input=None, in_cell_block=False, key=None, value=None):
                     raise ValueError(
                         f"Cell importance must be a number ≥ 0. {val.value} was given"
                     )
+                self._part_combos.append(self.particle_classifiers)
                 for particle in self.particle_classifiers:
                     self._particle_importances[particle] = value
         elif input:
@@ -52,6 +54,7 @@ def __init__(self, input=None, in_cell_block=False, key=None, value=None):
                     raise MalformedInputError(
                         input, f"Importances must be ≥ 0 value: {node} given"
                     )
+            self._part_combos.append(self.particle_classifiers)
             for particle in self.particle_classifiers:
                 self._particle_importances[particle] = copy.deepcopy(self._tree)
                 self._real_tree[particle] = copy.deepcopy(self._tree)
@@ -340,6 +343,48 @@ def _try_combine_values(self, new_vals, particle_pairings):
     def _update_cell_values(self):
         pass
 
+    @property
+    def trailing_comment(self):
+        """
+        The trailing comments and padding of an input.
+
+        Generally this will be blank as these will be moved to be a leading comment for the next input.
+
+        :returns: the trailing ``c`` style comments and intermixed padding (e.g., new lines)
+        :rtype: list
+        """
+        last_tree = list(self._real_tree.values())[-1]
+        if last_tree:
+            return last_tree.get_trailing_comment()
+
+    def _delete_trailing_comment(self):
+        for part, tree in reversed(self._real_tree.items()):
+            tree._delete_trailing_comment()
+            self.__delete_common_trailing(part)
+            break
+
+    def __delete_common_trailing(self, part):
+        to_delete = {part}
+        for combo_set in self._part_combos:
+            if part in combo_set:
+                to_delete |= combo_set
+        for part in to_delete:
+            self._particle_importances[part]["data"]._delete_trailing_comment()
+
+    def _grab_beginning_comment(self, new_padding):
+        last_tree = None
+        last_padding = None
+        for part, tree in self._particle_importances.items():
+            if last_padding is not None and last_tree is not None:
+                last_tree._grab_beginning_comment(last_padding)
+                self.__delete_common_trailing(part)
+            last_padding = tree.get_trailing_comment()
+            last_tree = tree
+        if new_padding:
+            next(iter(self._particle_importances.values()))[
+                "start_pad"
+            ]._grab_beginning_comment(new_padding)
+
 
 def _generate_default_data_tree(particle):
     list_node = syntax_node.ListNode("number sequence")
diff --git a/montepy/data_inputs/universe_input.py b/montepy/data_inputs/universe_input.py
index 291c8eed..f189f058 100644
--- a/montepy/data_inputs/universe_input.py
+++ b/montepy/data_inputs/universe_input.py
@@ -194,7 +194,7 @@ def __repr__(self):
             f"UNIVERSE: in_cell: {self._in_cell_block}"
             f" set_in_block: {self.set_in_cell_block}, "
             f"Universe : {self._universe}, "
-            f"Old Numbers: {self._old_numbers}"
+            f"Old Numbers: {self._old_numbers if hasattr(self, '_old_numbers') else ''}"
         )
         return ret
 
diff --git a/montepy/data_inputs/volume.py b/montepy/data_inputs/volume.py
index b78be685..e6db1e57 100644
--- a/montepy/data_inputs/volume.py
+++ b/montepy/data_inputs/volume.py
@@ -182,7 +182,7 @@ def __repr__(self):
         ret = (
             f"VOLUME: in_cell: {self._in_cell_block}, calc_by_mcnp: {self.is_mcnp_calculated},"
             f" set_in_block: {self.set_in_cell_block}, "
-            f"Volume : {self._volume}"
+            f"Volume : {self._volume if hasattr(self, '_volume') else ''}"
         )
         return ret
 
diff --git a/montepy/input_parser/cell_parser.py b/montepy/input_parser/cell_parser.py
index e728922f..65d347e6 100644
--- a/montepy/input_parser/cell_parser.py
+++ b/montepy/input_parser/cell_parser.py
@@ -112,12 +112,16 @@ def geometry_term(self, p):
         "geometry_term LOG_INTERPOLATE padding number_phrase",
     )
     def geometry_term(self, p):
-        shortcut = syntax_node.ShortcutNode(p)
+        shortcut = syntax_node.ShortcutNode(p, data_type=int)
         node_iter = iter(shortcut.nodes)
         if isinstance(p.geometry_term, syntax_node.GeometryTree):
             left = p.geometry_term
+            left.mark_last_leaf_shortcut(shortcut.type)
         else:
             left = next(node_iter)
+        left_type = None
+        if isinstance(left, syntax_node.ValueNode) and left == shortcut.nodes[0]:
+            left_type = shortcut.type
         for node in node_iter:
             new_tree = syntax_node.GeometryTree(
                 "intersection",
@@ -125,6 +129,8 @@ def geometry_term(self, p):
                 "*",
                 left,
                 node,
+                left_short_type=left_type,
+                right_short_type=shortcut.type,
             )
             left = new_tree
         return new_tree
diff --git a/montepy/input_parser/input_reader.py b/montepy/input_parser/input_reader.py
index 400d5e1f..4c508a01 100644
--- a/montepy/input_parser/input_reader.py
+++ b/montepy/input_parser/input_reader.py
@@ -3,15 +3,17 @@
 from montepy.constants import DEFAULT_VERSION
 
 
-def read_input(input_file, mcnp_version=DEFAULT_VERSION, replace=True):
+def read_input(destination, mcnp_version=DEFAULT_VERSION, replace=True):
     """
     Reads the specified MCNP Input file.
 
     The MCNP version must be a three component tuple e.g., (6, 2, 0) and (5, 1, 60).
 
+    .. note::
+        if a stream is provided. It will not be closed by this function.
 
-    :param input_file: the path to the input file to read.
-    :type input_file: str
+    :param destination: the path to the input file to read, or a readable stream.
+    :type destination: io.TextIOBase, str, os.PathLike
     :param mcnp_version: The version of MCNP that the input is intended for.
     :type mcnp_version: tuple
     :returns: The MCNP_Problem instance representing this file.
@@ -24,7 +26,7 @@ def read_input(input_file, mcnp_version=DEFAULT_VERSION, replace=True):
     :raises BrokenObjectLinkError: If a reference is made to an object that is not in the input file.
     :raises UnknownElement: If an isotope is specified for an unknown element.
     """
-    problem = mcnp_problem.MCNP_Problem(input_file)
+    problem = mcnp_problem.MCNP_Problem(destination)
     problem.mcnp_version = mcnp_version
     problem.parse_input(replace=replace)
     return problem
diff --git a/montepy/input_parser/material_parser.py b/montepy/input_parser/material_parser.py
index bb85bf75..1a932188 100644
--- a/montepy/input_parser/material_parser.py
+++ b/montepy/input_parser/material_parser.py
@@ -18,8 +18,8 @@ def material(self, p):
         for key, node in p.introduction.nodes.items():
             ret[key] = node
         ret["data"] = p.isotopes
-        if hasattr(p, "parameters"):
-            ret["parameters"] = p.parameters
+        if len(p) > 2:
+            ret["parameters"] = p[2]
         return syntax_node.SyntaxNode("data", ret)
 
     @_("isotope_fractions", "number_sequence", "isotope_hybrid_fractions")
diff --git a/montepy/input_parser/syntax_node.py b/montepy/input_parser/syntax_node.py
index c9775d90..ae9743b4 100644
--- a/montepy/input_parser/syntax_node.py
+++ b/montepy/input_parser/syntax_node.py
@@ -256,15 +256,24 @@ def comments(self):
             yield from node.comments
 
     def get_trailing_comment(self):
+        node = self._get_trailing_node()
+        if node:
+            return node.get_trailing_comment()
+
+    def _get_trailing_node(self):
         if len(self.nodes) == 0:
             return
         for node in reversed(self.nodes.values()):
             if node is not None:
-                return node.get_trailing_comment()
+                if isinstance(node, ValueNode):
+                    if node.value is not None:
+                        return node
+                elif len(node) > 0:
+                    return node
 
     def _delete_trailing_comment(self):
-        tail = next(reversed(self.nodes.items()))
-        tail[1]._delete_trailing_comment()
+        node = self._get_trailing_node()
+        node._delete_trailing_comment()
 
     def flatten(self):
         ret = []
@@ -283,6 +292,9 @@ class GeometryTree(SyntaxNodeBase):
     .. versionadded:: 0.2.0
         This was added with the major parser rework.
 
+    .. versionchanged:: 0.4.1
+        Added left/right_short_type
+
     :param name: a name for labeling this node.
     :type name: str
     :param tokens: The nodes that are in the tree.
@@ -293,28 +305,140 @@ class GeometryTree(SyntaxNodeBase):
     :type left: GeometryTree, ValueNode
     :param right: the node of the right side of the binary tree.
     :type right: GeometryTree, ValueNode
+    :param left_short_type: The type of Shortcut that right left leaf is involved in.
+    :type left_short_type: Shortcuts
+    :param right_short_type: The type of Shortcut that the right leaf is involved in.
+    :type right_short_type: Shortcuts
     """
 
-    def __init__(self, name, tokens, op, left, right=None):
+    def __init__(
+        self,
+        name,
+        tokens,
+        op,
+        left,
+        right=None,
+        left_short_type=None,
+        right_short_type=None,
+    ):
         super().__init__(name)
         assert all(list(map(lambda v: isinstance(v, SyntaxNodeBase), tokens.values())))
         self._nodes = tokens
         self._operator = Operator(op)
         self._left_side = left
         self._right_side = right
+        self._left_short_type = left_short_type
+        self._right_short_type = right_short_type
 
     def __str__(self):
-        return f"Geometry: ( {self._left_side} {self._operator} {self._right_side} )"
+        return (
+            f"Geometry: ( {self._left_side}"
+            f" {f'Short:{self._left_short_type.value}' if self._left_short_type else ''}"
+            f" {self._operator} {self._right_side} "
+            f"{f'Short:{self._right_short_type.value}' if self._right_short_type else ''})"
+        )
 
     def __repr__(self):
         return str(self)
 
     def format(self):
+        if self._left_short_type or self._right_short_type:
+            return self._format_shortcut()
         ret = ""
         for node in self.nodes.values():
             ret += node.format()
         return ret
 
+    def mark_last_leaf_shortcut(self, short_type):
+        """
+        Mark the final (rightmost) leaf node in this tree as being a shortcut.
+
+        :param short_type: the type of shortcut that this leaf is.
+        :type short_type: Shortcuts
+        """
+        if self.right is not None:
+            node = self.right
+            if self._right_short_type:
+                return
+        else:
+            node = self.left
+            if self._left_short_type:
+                return
+        if isinstance(node, type(self)):
+            return node.mark_last_leaf_shortcut(short_type)
+        if self.right is not None:
+            self._right_short_type = short_type
+        else:
+            self._left_short_type = short_type
+
+    def _flatten_shortcut(self):
+        """
+        Flattens this tree into a ListNode.
+
+        This will add ShortcutNodes as well.
+
+        :rtype: ListNode
+        """
+
+        def add_leaf(list_node, leaf, short_type):
+            end = list_node.nodes[-1] if len(list_node) > 0 else None
+
+            def flush_shortcut():
+                end.load_nodes(end.nodes)
+
+            def start_shortcut():
+                short = ShortcutNode(short_type=short_type)
+                # give an interpolate it's old beginning to give it right
+                # start value
+                if short_type in {
+                    Shortcuts.LOG_INTERPOLATE,
+                    Shortcuts.INTERPOLATE,
+                } and isinstance(end, ShortcutNode):
+                    short.append(end.nodes[-1])
+                    short._has_pseudo_start = True
+
+                short.append(leaf)
+                if not leaf.padding:
+                    leaf.padding = PaddingNode(" ")
+                list_node.append(short)
+
+            if short_type:
+                if isinstance(end, ShortcutNode):
+                    if end.type == short_type:
+                        end.append(leaf)
+                    else:
+                        flush_shortcut()
+                        start_shortcut()
+                else:
+                    start_shortcut()
+            else:
+                if isinstance(end, ShortcutNode):
+                    flush_shortcut()
+                    list_node.append(leaf)
+                else:
+                    list_node.append(leaf)
+
+        if isinstance(self.left, ValueNode):
+            ret = ListNode("list wrapper")
+            add_leaf(ret, self.left, self._left_short_type)
+        else:
+            ret = self.left._flatten_shortcut()
+        if self.right is not None:
+            if isinstance(self.right, ValueNode):
+                add_leaf(ret, self.right, self._right_short_type)
+            else:
+                [ret.append(n) for n in self.right._flatten_shortcut()]
+        return ret
+
+    def _format_shortcut(self):
+        """
+        Handles formatting a subset of tree that has shortcuts in it.
+        """
+        list_wrap = self._flatten_shortcut()
+        if isinstance(list_wrap.nodes[-1], ShortcutNode):
+            list_wrap.nodes[-1].load_nodes(list_wrap.nodes[-1].nodes)
+        return list_wrap.format()
+
     @property
     def comments(self):
         for node in self.nodes.values():
@@ -1674,13 +1798,14 @@ class ShortcutNode(ListNode):
     }
     _num_finder = re.compile(r"\d+")
 
-    def __init__(self, p=None, short_type=None):
+    def __init__(self, p=None, short_type=None, data_type=float):
         self._type = None
         self._end_pad = None
         self._nodes = collections.deque()
         self._original = []
         self._full = False
         self._num_node = ValueNode(None, float, never_pad=True)
+        self._data_type = data_type
         if p is not None:
             for search_strs, shortcut in self._shortcut_names.items():
                 for search_str in search_strs:
@@ -1711,6 +1836,25 @@ def __init__(self, p=None, short_type=None):
                 self._num_node = ValueNode(None, int, never_pad=True)
             self._end_pad = PaddingNode(" ")
 
+    def load_nodes(self, nodes):
+        """
+        Loads the given nodes into this shortcut, and update needed information.
+
+        For interpolate nodes should start and end with the beginning/end of
+        the interpolation.
+
+        :param nodes: the nodes to be loaded.
+        :type nodes: list
+        """
+        self._nodes = collections.deque(nodes)
+        if self.type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
+            self._begin = nodes[0].value
+            self._end = nodes[-1].value
+            if self.type == Shortcuts.LOG_INTERPOLATE:
+                self._begin = math.log10(self._begin)
+                self._end = math.log10(self._end)
+            self._spacing = (self._end - self._begin) / (len(nodes) - 1)
+
     @property
     def end_padding(self):
         """
@@ -1728,6 +1872,15 @@ def end_padding(self, padding):
             )
         self._end_pad = padding
 
+    @property
+    def type(self):
+        """
+        The Type of shortcut this ShortcutNode represents.
+
+        :rtype: Shortcuts
+        """
+        return self._type
+
     def __repr__(self):
         return f"(shortcut:{self._type}: {self.nodes})"
 
@@ -1765,6 +1918,11 @@ def _expand_multiply(self, p):
         self._num_node = ValueNode(mult_str, float, never_pad=True)
         if isinstance(p[0], ValueNode):
             last_val = self.nodes[-1]
+        elif isinstance(p[0], GeometryTree):
+            if "right" in p[0].nodes:
+                last_val = p[0].nodes["right"]
+            else:
+                last_val = p[0].nodes["left"]
         else:
             last_val = p[0].nodes[-1]
         if last_val.value is None:
@@ -1820,7 +1978,11 @@ def _expand_interpolate(self, p):
                 new_val = 10 ** (begin + spacing * (i + 1))
             else:
                 new_val = begin + spacing * (i + 1)
-            self.append(ValueNode(str(new_val), float, never_pad=True))
+            self.append(
+                ValueNode(
+                    str(self._data_type(new_val)), self._data_type, never_pad=True
+                )
+            )
         self._begin = begin
         self._end = end
         self._spacing = spacing
@@ -1834,8 +1996,7 @@ def _can_consume_node(self, node, direction, last_edge_shortcut=False):
         :type node: ValueNode
         :param direction: the direct to go in. Must be in {-1, 1}
         :type direction: int
-        :param last_edge_shortcut: Whether or the previous node in the list was
-            part of a different shortcut
+        :param last_edge_shortcut: Whether the previous node in the list was part of a different shortcut
         :type last_edge_shortcut: bool
         :returns: true it can be consumed.
         :rtype: bool
@@ -1937,8 +2098,10 @@ def format(self, leading_node=None):
         # repeat
         elif self._type == Shortcuts.REPEAT:
             temp = self._format_repeat(leading_node)
+        # multiply
         elif self._type == Shortcuts.MULTIPLY:
             temp = self._format_multiply(leading_node)
+        # interpolate
         elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
             temp = self._format_interpolate(leading_node)
         if self.end_padding:
@@ -1966,8 +2129,37 @@ def _format_jump(self):
 
         return f"{num_jumps.format()}{j}"
 
+    def _can_use_last_node(self, node, start=None):
+        """
+        Determine if the previous node can be used as the start to this node
+        (and therefore skip the start of this one).
+
+        Last node can be used if
+        - it's a basic ValueNode that matches this repeat
+        - it's also a shortcut, with the same edge values.
+
+        :param node: the previous node to test.
+        :type node: ValueNode, ShortcutNode
+        :param start: the starting value for this node (specifically for interpolation)
+        :type start: float
+        :returns: True if the node given can be used.
+        :rtype: bool
+        """
+        if isinstance(node, ValueNode):
+            value = node.value
+        elif isinstance(node, ShortcutNode):
+            value = node.nodes[-1].value
+        else:
+            return False
+        if value is None:
+            return False
+        if start is None:
+            start = self.nodes[0].value
+        return math.isclose(start, value)
+
     def _format_repeat(self, leading_node=None):
-        if leading_node is not None:
+
+        if self._can_use_last_node(leading_node):
             first_val = ""
             num_extra = 0
         else:
@@ -1990,23 +2182,29 @@ def _format_repeat(self, leading_node=None):
         return f"{first_val}{num_repeats.format()}{r}"
 
     def _format_multiply(self, leading_node=None):
-        if leading_node is not None:
+        # Multiply doesn't usually consume other nodes
+        if leading_node is not None and len(self) == 1:
             first_val = leading_node.nodes[-1]
             first_val_str = ""
         else:
             first_val = self.nodes[0]
             first_val_str = first_val
-        if "M" in self._original[-1]:
-            m = "M"
-        else:
+        if self._original and "m" in self._original[-1]:
             m = "m"
+        else:
+            m = "M"
         self._num_node.value = self.nodes[-1].value / first_val.value
         return f"{first_val_str.format()}{self._num_node.format()}{m}"
 
     def _format_interpolate(self, leading_node=None):
-        if leading_node is not None:
+        begin = self._begin
+        if self.type == Shortcuts.LOG_INTERPOLATE:
+            begin = 10**begin
+        if self._can_use_last_node(leading_node, begin):
             start = ""
             num_extra_nodes = 1
+            if hasattr(self, "_has_pseudo_start"):
+                num_extra_nodes += 1
         else:
             start = self.nodes[0]
             num_extra_nodes = 2
@@ -2161,7 +2359,8 @@ def __str__(self):
     def __repr__(self):
         return (
             f"(Classifier: mod: {self.modifier}, prefix: {self.prefix}, "
-            f"number: {self.number}, particles: {self.particles})"
+            f"number: {self.number}, particles: {self.particles},"
+            f" padding: {self.padding})"
         )
 
     @property
@@ -2171,6 +2370,14 @@ def comments(self):
         else:
             yield from []
 
+    def get_trailing_comment(self):
+        if self.padding:
+            return self.padding.get_trailing_comment()
+
+    def _delete_trailing_comment(self):
+        if self.padding:
+            self.padding._delete_trailing_comment()
+
     def flatten(self):
         ret = []
         if self.modifier:
@@ -2219,7 +2426,7 @@ def __init__(self):
         super().__init__("parameters")
         self._nodes = {}
 
-    def append(self, val):
+    def append(self, val, is_default=False):
         """
         Append the node to this node.
 
@@ -2228,6 +2435,8 @@ def append(self, val):
 
         :param val: the parameter to append.
         :type val: SyntaxNode
+        :param is_default: whether this parameter was added as a default tree not from the user.
+        :type is_default: bool
         """
         classifier = val["classifier"]
         key = (
@@ -2236,6 +2445,8 @@ def append(self, val):
         ).lower()
         if key in self._nodes:
             raise RedundantParameterSpecification(key, val)
+        if is_default:
+            val._is_default = True
         self._nodes[key] = val
 
     def __str__(self):
@@ -2257,12 +2468,16 @@ def format(self):
         return ret
 
     def get_trailing_comment(self):
-        tail = next(reversed(self.nodes.items()))
-        return tail[1].get_trailing_comment()
+        for node in reversed(self.nodes.values()):
+            if hasattr(node, "_is_default"):
+                continue
+            return node.get_trailing_comment()
 
     def _delete_trailing_comment(self):
-        tail = next(reversed(self.nodes.items()))
-        tail[1]._delete_trailing_comment()
+        for node in reversed(self.nodes.values()):
+            if hasattr(node, "_is_default"):
+                continue
+            node._delete_trailing_comment()
 
     @property
     def comments(self):
diff --git a/montepy/mcnp_problem.py b/montepy/mcnp_problem.py
index 2f1ae98b..a284064d 100644
--- a/montepy/mcnp_problem.py
+++ b/montepy/mcnp_problem.py
@@ -23,12 +23,18 @@ class MCNP_Problem:
     """
     A class to represent an entire MCNP problem in a semantic way.
 
-    :param file_name: the path to the file that will be read.
-    :type file_name: str
+    .. note::
+        If a stream is provided. It will not be closed by this function.
+
+    :param destination: the path to the input file to read, or a readable stream.
+    :type destination: io.TextIOBase, str, os.PathLike
     """
 
-    def __init__(self, file_name):
-        self._input_file = MCNP_InputFile(file_name)
+    def __init__(self, destination):
+        if hasattr(destination, "read") and callable(getattr(destination, "read")):
+            self._input_file = MCNP_InputFile.from_open_stream(destination)
+        elif isinstance(destination, (str, os.PathLike)):
+            self._input_file = MCNP_InputFile(destination)
         self._title = None
         self._message = None
         self._print_in_data_block = CellDataPrintController()
@@ -244,6 +250,7 @@ def parse_input(self, check_input=False, replace=True):
         """
         trailing_comment = None
         last_obj = None
+        last_block = None
         OBJ_MATCHER = {
             block_type.BlockType.CELL: (Cell, self._cells),
             block_type.BlockType.SURFACE: (
@@ -266,6 +273,9 @@ def parse_input(self, check_input=False, replace=True):
                     self._title = input
 
                 elif isinstance(input, mcnp_input.Input):
+                    if last_block != input.block_type:
+                        trailing_comment = None
+                        last_block = input.block_type
                     obj_parser, obj_container = OBJ_MATCHER[input.block_type]
                     if len(input.input_lines) > 0:
                         try:
diff --git a/montepy/surfaces/cylinder_on_axis.py b/montepy/surfaces/cylinder_on_axis.py
index c557d39f..0ade7c00 100644
--- a/montepy/surfaces/cylinder_on_axis.py
+++ b/montepy/surfaces/cylinder_on_axis.py
@@ -44,7 +44,7 @@ def radius(self):
 
     def validate(self):
         super().validate()
-        if not self.radius:
+        if self.radius is None:
             raise IllegalState(f"Surface: {self.number} does not have a radius set.")
 
     def find_duplicate_surfaces(self, surfaces, tolerance):


Test patch:
diff --git a/tests/inputs/test_complement_edge.imcnp b/tests/inputs/test_complement_edge.imcnp
index 5d39a8a4..e83b83fc 100644
--- a/tests/inputs/test_complement_edge.imcnp
+++ b/tests/inputs/test_complement_edge.imcnp
@@ -11,6 +11,7 @@ C Bounding water outer capsule
      U=6000 IMP:N=1
 61441 814 0.087
       (-61867 61870 -61759):(-61868 61869 -61759): (-61870 61868 -61759 61758)
+      IMP:N=0
 
 1 SO 5.0
 2 PZ 0.0
diff --git a/tests/inputs/test_importance.imcnp b/tests/inputs/test_importance.imcnp
index b279bb2e..ef0c8abe 100644
--- a/tests/inputs/test_importance.imcnp
+++ b/tests/inputs/test_importance.imcnp
@@ -41,7 +41,8 @@ MT3 lwtr.23t
 C execution
 ksrc 0 0 0
 imp:n,p 1 1 1 0 3
-imp:e   0 0 0 1 2
+c special comment related to #520
+imp:e   0 2r 1 r
 kcode 100000 1.000 50 1050
 phys:p j 1 2j 1
 mode n p e
diff --git a/tests/inputs/test_trail_comment_edge.imcnp b/tests/inputs/test_trail_comment_edge.imcnp
new file mode 100644
index 00000000..b921151e
--- /dev/null
+++ b/tests/inputs/test_trail_comment_edge.imcnp
@@ -0,0 +1,11 @@
+Model with issues trailing comments
+100103  3926  5.00000E-02 (-1          -1  1):   $ Lower Rodlet Sodium Bond
+                          (-1  1  -1   1)    u=424 
+c
+c comment for next cell... 
+c
+2 0 -1
+
+1 SO 5
+
+M3926 1001.80c 1.0
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index e9cf3ca5..4745d76e 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -3,6 +3,7 @@
 import io
 import montepy
 from montepy.errors import *
+from pathlib import Path
 import os
 
 import pytest
@@ -231,3 +232,10 @@ def test_universe_after_comment():
             os.remove(out_file)
         except FileNotFoundError:
             pass
+
+
+def test_trailing_comment_edge():
+    problem = montepy.read_input(
+        Path("tests") / "inputs" / "test_trail_comment_edge.imcnp"
+    )
+    assert len(problem.cells[2].leading_comments) == 3
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 10ab35f6..b50d0a65 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,6 +1,7 @@
 # Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
 import copy
 import io
+from pathlib import Path
 
 import pytest
 import os
@@ -17,6 +18,8 @@
 from montepy.particle import Particle
 import numpy as np
 
+from tests import constants
+
 
 @pytest.fixture(scope="module")
 def simple_problem():
@@ -68,6 +71,17 @@ def test_lazy_comments_check(simple_problem):
     print(material2._tree.get_trailing_comment())
 
 
+def test_moving_trail_comments(universe_problem):
+    problem = copy.deepcopy(universe_problem)
+    mat = problem.materials[2]
+    idx = problem.data_inputs.index(mat)
+    dat = problem.data_inputs[idx - 1]
+    assert len(mat.comments) == 1
+    assert len(mat.leading_comments) == 1
+    assert len(dat.leading_comments) == 0
+    assert len(dat.comments) == 0
+
+
 def test_material_parsing(simple_problem):
     mat_numbers = [1, 2, 3]
     for i, mat in enumerate(simple_problem.materials):
@@ -479,9 +493,9 @@ def test_importance_format_unmutated(importance_problem):
     imp = importance_problem.cells._importance
     output = imp.format_for_mcnp_input((6, 2, 0))
     print(output)
-    assert len(output) == 2
+    assert len(output) == 3
     assert "imp:n,p 1 1 1 0 3" == output[0]
-    assert "imp:e   0 0 0 1 2" == output[1]
+    assert "imp:e   0 2r 1 r" == output[2]
 
 
 def test_importance_format_mutated(importance_problem):
@@ -491,8 +505,9 @@ def test_importance_format_mutated(importance_problem):
     with pytest.warns(LineExpansionWarning):
         output = imp.format_for_mcnp_input((6, 2, 0))
     print(output)
-    assert len(output) == 3
+    assert len(output) == 4
     assert "imp:n 0.5 1 1 0 3" in output
+    assert "c special comment related to #520" == output[2]
 
 
 def test_importance_write_unmutated(importance_problem):
@@ -1066,3 +1081,60 @@ def test_alternate_encoding():
     montepy.read_input(
         os.path.join("tests", "inputs", "bad_encoding.imcnp"), replace=True
     )
+
+
+_SKIP_LINES = {
+    # skip lines of added implied importances
+    "tests/inputs/test_universe_data.imcnp": {5: True, 14: True, 15: True},
+}
+
+
+@pytest.mark.parametrize(
+    "file",
+    set((Path("tests") / "inputs").iterdir())
+    - {
+        Path("tests")
+        / "inputs"
+        / p  #                           Skip complexity of read
+        for p in constants.BAD_INPUTS
+        | constants.IGNORE_FILES
+        | {"testRead.imcnp", "readEdgeCase.imcnp"}
+    },
+)
+def test_read_write_cycle(file):
+    print(f"Testing against {file} *********************")
+    problem = montepy.read_input(file)
+    SKIPPERS = _SKIP_LINES.get(str(file), {})
+    fh = io.StringIO()
+    problem.write_problem(fh)
+    fh.seek(0)
+    # test valid syntax
+    new_problem = montepy.MCNP_Problem("foo")
+    # verify lines are similar
+    fh.seek(0)
+    lines = [line.rstrip() for line in fh]
+    [print(line) for line in lines]
+    with open(file, "r") as gold_fh:
+        gold_fh_iter = iter(gold_fh)
+        lines_iter = iter(lines)
+        for i, (gold_line, new_line) in enumerate(zip(gold_fh_iter, lines_iter)):
+            if i in SKIPPERS:
+                # True means skip new file line
+                if SKIPPERS[i]:
+                    new_line = next(lines_iter)
+                else:
+                    gold_line = next(gold_fh_iter)
+            # edge case override for not fixing #527.
+            if str(file) == "tests/inputs/test_interp_edge.imcnp" and i == 1:
+                assert new_line == "10214   0    (1  2I 4 )"
+                continue
+            try:
+                assert new_line == gold_line.rstrip().expandtabs(8)
+            except AssertionError as e:
+                # handle case of making importance explicit
+                if "IMP:n=0.0" in new_line:
+                    assert (
+                        new_line.replace("IMP:n=0.0", "").rstrip() == gold_line.rstrip()
+                    )
+                else:
+                    raise e
diff --git a/tests/test_syntax_parsing.py b/tests/test_syntax_parsing.py
index e4411d32..5353237a 100644
--- a/tests/test_syntax_parsing.py
+++ b/tests/test_syntax_parsing.py
@@ -11,6 +11,7 @@
 from montepy.input_parser.block_type import BlockType
 from montepy.input_parser.input_file import MCNP_InputFile
 from montepy.input_parser.parser_base import MCNP_Parser
+from montepy.input_parser.shortcuts import Shortcuts
 from montepy.input_parser import syntax_node
 from montepy.particle import Particle
 import warnings
@@ -485,6 +486,38 @@ def test_geometry_comments(self):
         self.assertEqual(len(comments), 1)
 
 
+def test_geometry_tree_mark_last_leaf_shortcut():
+    # test left leaf
+    tree = {
+        "left": syntax_node.ValueNode("123", int),
+        "operator": syntax_node.PaddingNode(" "),
+    }
+    geom = syntax_node.GeometryTree(
+        "test", tree, montepy.Operator.INTERSECTION, tree["left"]
+    )
+    geom.mark_last_leaf_shortcut(Shortcuts.REPEAT)
+    assert geom._left_short_type == Shortcuts.REPEAT
+    # try overriding old shortcut
+    geom.mark_last_leaf_shortcut(Shortcuts.MULTIPLY)
+    assert geom._left_short_type == Shortcuts.REPEAT
+    tree["right"] = syntax_node.ValueNode("789", int)
+    geom = syntax_node.GeometryTree(
+        "test", tree, montepy.Operator.INTERSECTION, tree["left"], tree["right"]
+    )
+    del tree["right"]
+    tree["left"] = syntax_node.ValueNode("456", int)
+    # make unbalanced tree in the other way for rigor
+    geom = syntax_node.GeometryTree(
+        "test", tree, montepy.Operator.INTERSECTION, tree["left"], geom
+    )
+    # test a right-side leaf
+    geom.mark_last_leaf_shortcut(Shortcuts.REPEAT)
+    assert geom.right._right_short_type == Shortcuts.REPEAT
+    # try overriding old shortcut
+    geom.mark_last_leaf_shortcut(Shortcuts.MULTIPLY)
+    assert geom.right._right_short_type == Shortcuts.REPEAT
+
+
 class TestPaddingNode(TestCase):
     def test_padding_init(self):
         pad = syntax_node.PaddingNode(" ")
@@ -823,120 +856,178 @@ def test_shortcut_end_padding_setter(self):
         with self.assertRaises(TypeError):
             short.end_padding = " "
 
-    """
-    Most examples, unless otherwise noted are taken from Section 2.8.1
-    of LA-UR-17-29981.
-    """
-    tests = {
-        "1 3M 2r": [1, 3, 3, 3],
-        # unofficial
-        "0.01 2ILOG 10": [0.01, 0.1, 1, 10],
-        "1 3M I 4": [1, 3, 3.5, 4],
-        "1 3M 3M": [1, 3, 9],
-        "1 2R 2I 2.5": [1, 1, 1, 1.5, 2, 2.5],
-        "1 R 2m": [1, 1, 2],
-        "1 R R": [1, 1, 1],
-        "1 2i 4 3m": [1, 2, 3, 4, 12],
-        # unofficial
-        "1 i 3": [1, 2, 3],
-        # unofficial
-        "1 ilog 100": [1, 10, 100],
-        # last official one
-        "1 2i 4 2i 10": [
-            1,
-            2,
-            3,
-            4,
-            6,
-            8,
-            10,
-        ],
-        "1 2j 4": [1, montepy.Jump(), montepy.Jump(), 4],
-        "1 j": [1, montepy.Jump()],
-    }
 
-    def test_shortcut_expansion(self):
-        invalid = [
-            "3J 4R",
-            "1 4I 3M",
-            # last official test
-            "1 4I J",
-            "1 2Ilog J",
-            "J 2Ilog 5",
-            "3J 2M",
-            "10 M",
-            "2R",
-        ]
+"""
+Most examples, unless otherwise noted are taken from Section 2.8.1
+of LA-UR-17-29981.
+"""
+tests = {
+    "1 3M 2r": [1, 3, 3, 3],
+    # unofficial
+    "0.01 2ILOG 10": [0.01, 0.1, 1, 10],
+    "1 3M I 4": [1, 3, 3.5, 4],
+    "1 3M 3M": [1, 3, 9],
+    "1 2R 2I 2.5": [1, 1, 1, 1.5, 2, 2.5],
+    "1 R 2m": [1, 1, 2],
+    "1 R R": [1, 1, 1],
+    "1 2i 4 3m": [1, 2, 3, 4, 12],
+    # unofficial
+    "1 i 3": [1, 2, 3],
+    # unofficial
+    "1 ilog 100": [1, 10, 100],
+    "1 1r ilog 100": [1, 1, 10, 100],
+    # last official one
+    "1 2i 4 2i 10": [
+        1,
+        2,
+        3,
+        4,
+        6,
+        8,
+        10,
+    ],
+    "1 2j 4": [1, montepy.Jump(), montepy.Jump(), 4],
+    "1 j": [1, montepy.Jump()],
+}
+
+
+@pytest.mark.parametrize("test, answer", tests.items())
+def test_shortcut_expansion_valid(test, answer):
+    parser = ShortcutTestFixture()
+    print(test)
+    input = Input([test], BlockType.DATA)
+    parsed = parser.parse(input.tokenize())
+    print(parsed)
+    for val, gold in zip(parsed, answer):
+        if val.value is None:
+            assert gold == montepy.Jump()
+        else:
+            assert val.value == pytest.approx(gold)
+    assert parsed.format() == test
 
-        parser = ShortcutTestFixture()
-        for test, answer in self.tests.items():
-            print(test)
-            input = Input([test], BlockType.DATA)
-            parsed = parser.parse(input.tokenize())
-            for val, gold in zip(parsed, answer):
-                if val.value is None:
-                    self.assertEqual(gold, montepy.Jump())
-                else:
-                    self.assertAlmostEqual(val.value, gold)
-        for test in invalid:
-            print(test)
-            with self.assertRaises(ValueError):
-                input = Input([test], BlockType.DATA)
-                parsed = parser.parse(input.tokenize())
-                if parsed is None:
-                    raise montepy.errors.MalformedInputError("", "")
-
-    def test_shortcut_geometry_expansion(self):
-        tests = {
-            "1 3r ": [1, 1, 1, 1],
-            "1 1 3r ": [1, 1, 1, 1, 1],
-            "1 -2M ": [1, -2],
-            "1 2i 4 ": [1, 2, 3, 4],
-            "1 1 2i 4 ": [1, 1, 2, 3, 4],
-            "1 ilog 100 ": [1, 10, 100],
-            # secretly test iterator
-            "#1": [1],
-            "#(1 2 3)": [1, 2, 3],
-            "1 2:( 3 4 5)": [1, 2, 3, 4, 5],
-        }
 
-        parser = ShortcutGeometryTestFixture()
-        for test, answer in tests.items():
-            print(test)
-            input = Input([test], BlockType.CELL)
-            parsed = parser.parse(input.tokenize())
-            for val, gold in zip(parsed, answer):
-                self.assertAlmostEqual(val.value, gold)
-
-    def test_shortcut_format(self):
-        parser = ShortcutTestFixture()
-        for in_str, answer in [
-            ("1 5R", "1 5R"),
-            ("1 5r", "1 5r"),
-            ("1 r", "1 r"),
-            ("1 2i 5", "1 2i 5"),
-            ("1 2ilog 5", "1 2ilog 5"),
-            ("1 r 2i 5", "1 r 2i 5"),
-            ("1 r 2ilog 5", "1 r 2ilog 5"),
-            ("1 5M", "1 5M"),
-            ("1 5m", "1 5m"),
-            ("2j ", "2j "),
-            ("2j", "2j"),
-            ("2J ", "2J "),
-            ("J", "J"),
-        ]:
-            print(in_str, answer)
-            input = Input([in_str], BlockType.CELL)
-            shortcut = parser.parse(input.tokenize())
-            self.assertEqual(shortcut.format(), answer)
-        # try jump with empty jump shortcut
-        shortcut.nodes.clear()
-        self.assertEqual(shortcut.format(), "")
-        for in_str in self.tests.keys():
-            print(in_str)
-            input = Input([in_str], BlockType.CELL)
-            shortcut = parser.parse(input.tokenize())
-            self.assertEqual(shortcut.format(), in_str)
+@pytest.mark.parametrize(
+    "test",
+    [
+        ("3J 4R"),
+        ("1 4I 3M"),
+        # last official test
+        ("1 4I J"),
+        ("1 2Ilog J"),
+        ("J 2Ilog 5"),
+        ("3J 2M"),
+        ("10 M"),
+        ("2R"),
+    ],
+)
+def test_shortcut_expansion_invalid(test):
+    print(test)
+    parser = ShortcutTestFixture()
+    with pytest.raises(ValueError):
+        input = Input([test], BlockType.DATA)
+        parsed = parser.parse(input.tokenize())
+        if parsed is None:
+            raise montepy.errors.MalformedInputError("", "")
+
+
+@pytest.mark.parametrize(
+    "test, answer, form_ans",
+    [
+        ("1 3r ", [1, 1, 1, 1], None),
+        ("1 1 3r ", [1, 1, 1, 1, 1], None),
+        ("1 1 2M 3r ", [1, 1, 2, 2, 2, 2], None),
+        ("1 -2M ", [1, -2], None),
+        ("1 2i 4 ", [1, 2, 3, 4], None),
+        ("1 2i 4 ", [1, 2, 3, 4], None),
+        ("1 1 2i 4 ", [1, 1, 2, 3, 4], None),
+        ("1 1 2i 4 5 6 ", [1, 1, 2, 3, 4, 5, 6], "1 1 2I 4  5 6"),
+        ("1 1 2i 4:5 6 ", [1, 1, 2, 3, 4, 5, 6], "1 1 2I 4 :5 6"),
+        ("1 ilog 100 ", [1, 10, 100], "1 1ILOG 100"),
+        ("1 1r ilog 100 ", [1, 1, 10, 100], "1 1R 1ILOG 100"),
+        # secretly test iterator
+        ("#1", [1], None),
+        ("#(1 2 3)", [1, 2, 3], None),
+        ("1 2:( 3 4 5)", [1, 2, 3, 4, 5], None),
+    ],
+)
+def test_shortcut_geometry_expansion(test, answer, form_ans):
+
+    parser = ShortcutGeometryTestFixture()
+    print(test)
+    input = Input([test], BlockType.CELL)
+    parsed = parser.parse(input.tokenize())
+    for val, gold in zip(parsed, answer):
+        assert val.value == gold
+    if form_ans:
+        assert parsed.format().rstrip() == form_ans
+    else:
+        assert parsed.format().rstrip() == test.upper().rstrip()
+
+
+@pytest.mark.parametrize(
+    "test, length, indices",
+    [
+        ("1 3r ", 1, {0: Shortcuts.REPEAT}),
+        ("1 1 3r ", 2, {1: Shortcuts.REPEAT}),
+        ("1 1 2M 3r ", 3, {1: Shortcuts.MULTIPLY, 2: Shortcuts.REPEAT}),
+        ("1 -2M ", 1, {0: Shortcuts.MULTIPLY}),
+        ("1 2i 4 ", 1, {0: Shortcuts.INTERPOLATE}),
+        ("1 2i 4 ", 1, {0: Shortcuts.INTERPOLATE}),
+        ("1 1 2i 4 ", 2, {1: Shortcuts.INTERPOLATE}),
+        ("1 1 2i 4 5 6 ", 4, {1: Shortcuts.INTERPOLATE}),
+        ("1 1 2i 4:5 6 ", 4, {1: Shortcuts.INTERPOLATE}),
+        ("1 ilog 100 ", 1, {0: Shortcuts.LOG_INTERPOLATE}),
+        # secretly test iterator
+        ("#1", 1, {}),
+        ("#(1 2 3)", 3, {}),
+        ("1 2:( 3 4 5)", 5, {}),
+    ],
+)
+def test_shortcut_flatten(test, length, indices):
+
+    parser = ShortcutGeometryTestFixture()
+    print(test)
+    input = Input([test], BlockType.CELL)
+    parsed = parser.parse(input.tokenize())
+    flatpack = parsed._flatten_shortcut()
+    assert len(flatpack) == length
+    for index, short_type in indices.items():
+        assert isinstance(flatpack.nodes[index], syntax_node.ShortcutNode)
+        assert flatpack.nodes[index].type == short_type
+
+
+@pytest.mark.parametrize(
+    "in_str, answer",
+    [
+        ("1 5R", "1 5R"),
+        ("1 5r", "1 5r"),
+        ("1 5r 2m", "1 5r 2m"),
+        ("1 r", "1 r"),
+        ("1 r 2 2r", "1 r 2 2r"),
+        ("1 J 5 2R", "1 J 5 2R"),
+        ("1 2i 5", "1 2i 5"),
+        ("1 2ilog 5", "1 2ilog 5"),
+        ("1 r 2i 5", "1 r 2i 5"),
+        ("1 2i 5 r", "1 2i 5 r"),
+        ("1 r 2ilog 5", "1 r 2ilog 5"),
+        ("1 5M", "1 5M"),
+        ("1 5m", "1 5m"),
+        ("1 5m 2r", "1 5m 2r"),
+        ("2j ", "2j "),
+        ("2j", "2j"),
+        ("2J ", "2J "),
+        ("J", "J"),
+        ("0 2i 3 10 2i 16 1", "0 2i 3 10 2i 16 1"),
+    ],
+)
+def test_shortcut_format(in_str, answer):
+    parser = ShortcutTestFixture()
+    input = Input([in_str], BlockType.CELL)
+    shortcut = parser.parse(input.tokenize())
+    assert shortcut.format() == answer
+    # try jump with empty jump shortcut
+    shortcut.nodes.clear()
+    assert shortcut.format() == ""
 
 
 class ShortcutTestFixture(MCNP_Parser):