Skip to content

fix(surface): preserve original case of surface type mnemonics#929

Open
novavale wants to merge 4 commits intoidaholab:developfrom
novavale:fix/preserve-surface-case
Open

fix(surface): preserve original case of surface type mnemonics#929
novavale wants to merge 4 commits intoidaholab:developfrom
novavale:fix/preserve-surface-case

Conversation

@novavale
Copy link

@novavale novavale commented Mar 14, 2026

Description

Fixes #522

MCNP surface type mnemonics are case-insensitive, so users may write sO, So, or so instead of SO. Previously MontePy always wrote them back in uppercase, silently changing the user's input.

Root cause: ValueNode.convert_to_enum(switch_to_upper=True) did not set _og_value after resolving the enum. As a result, _value_changed always returned True (because _og_value was None), causing format() to fall through to the enum-formatting path — which emits value.value (always uppercase, e.g. "SO") instead of returning the original token verbatim.

Fix: one line in convert_to_enum — set self._og_value = self._value after the enum is resolved. This mirrors the identical pattern in convert_to_int (line 1011). When the user hasn't changed the surface type, _value_changed is False and format() returns the original token, preserving case.

Before:

1    sO 0     → written as: 1    SO 0

After:

1    sO 0     → written as: 1    sO 0   ✓

General Checklist

  • I have performed a self-review of my own code.
  • The code follows the standards outlined in the development documentation.
  • I have formatted my code with black version 25 or 26.
  • I have added tests that prove my fix is effective or that my feature works (if applicable).

LLM Disclosure

  1. Are you?

    • A human user
    • A large language model (LLM), including ones acting on behalf of a human
  2. Were any large language models (LLM or "AI") used in to generate any of this code?

  • Yes
    • Model(s) used: Claude (claude-sonnet-4-6)
  • No

Documentation Checklist

  • I have documented all added classes and methods.
  • For infrastructure updates, I have updated the developer's guide.
  • For significant new features, I have added a section to the getting started guide.

Additional Notes for Reviewers

  • Change is a single line in syntax_node.py:convert_to_enum — minimal surface area
  • 540 tests pass; 1 pre-existing failure in test_cells_clone (known hypothesis flakiness, unrelated to this change)
  • The fix applies to all enum conversions via convert_to_enum, which in MontePy is called from surface.py (surface type) and lattice_input.py (lattice type, integer-based). Both benefit from the same case/value preservation logic.

📚 Documentation preview 📚: https://montepy--929.org.readthedocs.build/en/929/

Nova Vale and others added 2 commits March 14, 2026 08:05
Fixes idaholab#522

MCNP surface mnemonics are case-insensitive, so a user may write 'sO'
or 'So' instead of 'SO'. Previously MontePy uppercased the token in
convert_to_enum(switch_to_upper=True), which caused the original case
to be lost when the problem was written back out.

Root cause: convert_to_enum did not set _og_value after resolving the
enum, so ValueNode._value_changed always returned True (since _og_value
was None), causing format() to re-format via the enum's .value (always
uppercase) instead of returning the preserved original token.

Fix: set self._og_value = self._value at the end of convert_to_enum.
This mirrors the pattern in convert_to_int (line 1011). When the user
has not changed the surface type, _value_changed is False and format()
returns the original token verbatim, preserving case.

Tests: 7 parametrized cases in test_surface_preserves_mnemonic_case
(so, sO, So, SO, pz, PZ, Pz). 540 tests pass; 1 pre-existing failure
in test_cells_clone (unrelated hypothesis flakiness, documented).
Copy link
Collaborator

@MicahGale MicahGale left a comment

Choose a reason for hiding this comment

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

A few questions. Also the failing tests need to be resolved first.

if not (allow_none and self._value is None):
self._value = enum_class(value)
self._formatter = self._FORMATTERS[format_type].copy()
self._og_value = self._value
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this have been set when the ValueNode is first initialized?

def test_surface_preserves_mnemonic_case(in_str, expected):
"""Surface mnemonics should be written with the original case (issue #522)."""
surf = Surface(in_str)
result = surf.format_for_mcnp_input((6, 2, 0))[0]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use surf.mcnp_str()

Nova Vale added 2 commits March 14, 2026 18:23
Per MicahGale's review:

1. '_og_value should be set when ValueNode is first initialized' —
   Reverted the convert_to_enum change. __init__ already sets
   _og_value = self.value (the raw string token). Instead, extended
   _value_changed to handle enum types: when _type is an Enum subclass
   and the canonical value matches _og_value case-insensitively (e.g.
   SurfaceType.SO vs 'sO'), the field is treated as unchanged and
   format() returns the original token verbatim.

2. 'Use surf.mcnp_str()' — Updated test to call surf.mcnp_str()
   instead of surf.format_for_mcnp_input((6, 2, 0))[0].

3. Added changelog entry under #Next Version# / Bugs Fixed.

Note: doc-build 'broken hyperlinks' failure is pre-existing on develop
(confirmed by checking develop branch CI runs) and unrelated to this PR.
@novavale
Copy link
Author

Thanks for the review — addressed in 4a10187.

"Shouldn't this have been set when the ValueNode is first initialized?"

You're right. __init__ already sets _og_value = self.value (the raw string token, e.g. "sO"), so there's no need to overwrite it in convert_to_enum. I reverted that change.

Instead, I extended _value_changed to handle enum types: when _type is an enum.Enum subclass, the comparison is done case-insensitively between self.value.value (the canonical enum string, e.g. "SO") and self._og_value (the original token, e.g. "sO"). If they match modulo case, the field is considered unchanged and format() returns the original token verbatim. If the user changes the surface type (e.g. to SurfaceType.PZ), _value_changed returns True as expected.

"Use surf.mcnp_str()"

Updated.

Failing tests

changelog-test — added a changelog entry under #Next Version# / Bugs Fixed.

doc-build — the failure is in "Test for broken hyperlinks", which is pre-existing on develop itself (confirmed: the most recent develop CI run also fails this step). It's unrelated to this PR.

Comment on lines +1267 to +1269
if isinstance(self._og_value, str)
else str(self._og_value)
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What cases are you guarding against for _og_value not being a string? None was previously checked.

@MicahGale
Copy link
Collaborator

In the failed tests it appears you can't always assume you are working with a string based Enum. I'm not sure if we explicitly use StrEnum.

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.

Preserve case when writing surface cards

2 participants