Skip to content

Commit df6a1fe

Browse files
committed
feat: extend LATEX_VAR_TOKEN_RE to support up to 5 levels of nested braces and add related tests
1 parent ea57329 commit df6a1fe

5 files changed

Lines changed: 62 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.6.29][0.6.29] - 2026-03-23
9+
10+
### Fixed
11+
12+
- Extended `LATEX_VAR_TOKEN_RE` regex in `patterns.py` to support up to 5 levels of nested braces in subscripts (previously only 1 level)
13+
- Now `MonteCarlo.run()` can handle expressions contain deeply nested subscript variables
14+
- Added composable brace-nesting helpers (`_BRACE_L0`..`_BRACE_L4`) for readable regex construction
15+
- Added deep nesting test cases to `test_data.py`, `test_patterns.py`, and `test_parser.py`
16+
817
## [0.6.28][0.6.28] - 2026-03-19
918

1019
### Fixed
@@ -801,6 +810,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
801810

802811
Experimentation phase before formal semantic versioning, establishing core dimensional analysis concepts and data structures.
803812

813+
[0.6.29]: https://github.com/DASA-Design/PyDASA/compare/v0.6.28...v0.6.29
804814
[0.6.28]: https://github.com/DASA-Design/PyDASA/compare/v0.6.27...v0.6.28
805815
[0.6.27]: https://github.com/DASA-Design/PyDASA/compare/v0.6.26...v0.6.27
806816
[0.6.26]: https://github.com/DASA-Design/PyDASA/compare/v0.6.25...v0.6.26

src/pydasa/validations/patterns.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,25 @@
2424
"""
2525

2626
# :attr: LATEX_VAR_TOKEN_RE
27+
# Brace nesting helpers – each level wraps the previous one so that
28+
# the overall pattern supports up to 5 levels of nested braces inside
29+
# subscripts (e.g. M_{a*(c*t_{R_{P*(A*(C*S))}})}).
30+
_BRACE_L0: str = r"[^{}]*"
31+
_BRACE_L1: str = r"(?:[^{}]|\{" + _BRACE_L0 + r"\})*"
32+
_BRACE_L2: str = r"(?:[^{}]|\{" + _BRACE_L1 + r"\})*"
33+
_BRACE_L3: str = r"(?:[^{}]|\{" + _BRACE_L2 + r"\})*"
34+
_BRACE_L4: str = r"(?:[^{}]|\{" + _BRACE_L3 + r"\})*"
35+
2736
LATEX_VAR_TOKEN_RE: str = (
2837
r"(\\[A-Za-z]+|[A-Za-z][A-Za-z0-9]*)"
29-
r"(?:_(?:[A-Za-z0-9]+|\{(?:[^{}]|\{[^{}]*\})+\}))?"
38+
r"(?:_(?:[A-Za-z0-9]+|\{" + _BRACE_L4 + r"\}))?"
3039
)
3140
"""
3241
Regex pattern to match LaTeX-like variable tokens with optional subscripts,
33-
including one nested brace level inside subscripts.
42+
supporting up to 5 levels of nested braces inside subscripts.
3443
3544
Examples:
36-
'\\alpha', '\\mu_{1}', 'M_{buf_{AS}}'
45+
'\\alpha', '\\mu_{1}', 'M_{buf_{AS}}', 'M_{a*(c*t_{R_{P*(A*(C*S))}})}'
3746
"""
3847

3948
# NOTE: OG REGEX!

tests/pydasa/data/test_data.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ def get_config_test_data():
3232
"DC_CAT_KEYS": ["COMPUTED", "DERIVED"],
3333
"SENS_ANSYS_KEYS": ["SYM", "NUM"],
3434
"VALID_LATEX": ["alpha", "\\alpha", "beta_1", "\\beta_{1}", "\\Pi_{0}"],
35-
"VALID_LATEX_VAR_TOKEN": ["M_{buf_{AS}}", "M_{a*(c*t_{A*S})}", "\\alpha", "\\mu_{1}"],
35+
"VALID_LATEX_VAR_TOKEN": [
36+
"M_{buf_{AS}}", "M_{a*(c*t_{A*S})}", "\\alpha", "\\mu_{1}",
37+
"M_{a*(c*t_{R_{PACS}})}", "M_{a*(c*t_{R_{P*(A*(C*S))}})}"
38+
],
3639
"VALID_DIMENSIONS": ["M", "L*T", "M*L^-1*T^-2", "L^2*T^-1", "T^-1"],
3740
"INVALID_DIMENSIONS": ["X", "M*X", "M**2", "M^2.5", "M L", ""],
3841
"PHYSICAL_DIMS": [
@@ -162,8 +165,13 @@ def get_latex_test_data():
162165
},
163166
"COMPLEX_EXPR": "\\alpha + \\beta_{1} + \\gamma_{2}",
164167
"NESTED_SUBSCRIPT_CASES": [
168+
# 1-level nesting
165169
("M_{act_{AS}}", "M_act_AS"),
166170
("M_{buf_{AS}}", "M_buf_AS"),
171+
# 2-level nesting
172+
("M_{a*(c*t_{R_{PACS}})}", "M_a*(c*t_R_PACS)"),
173+
# 3-level nesting (the real-world failure case)
174+
("M_{a*(c*t_{R_{P*(A*(C*S))}})}", "M_a*(c*t_R_P*(A*(C*S)))"),
167175
],
168176
"PHYSICS_EXPR": "\\frac{U * y_{2}}{d} + \\frac{P * d^{2}}{\\mu_{1} * U}",
169177
"DIMENSIONAL_CASES": [

tests/pydasa/serialization/test_parser.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,20 @@ def test_expression_with_powers(self) -> None:
318318
py_vars = list(latex_to_py.values())
319319
assert any("d" in v for v in py_vars)
320320
assert any("P" in v for v in py_vars)
321+
322+
def test_deeply_nested_subscript_real_world(self) -> None:
323+
"""Test extraction from deeply nested subscript (real-world failure case)."""
324+
expr = "M_{a*(c*t_{R_{P*(A*(C*S))}})}"
325+
latex_to_py, py_to_latex = extract_latex_vars(expr)
326+
327+
assert expr in latex_to_py, f"Token not found in latex_to_py: {expr}"
328+
assert latex_to_py[expr] == "M_a*(c*t_R_P*(A*(C*S)))"
329+
330+
def test_deeply_nested_create_mapping(self) -> None:
331+
"""Test create_latex_mapping handles deeply nested subscripts end-to-end."""
332+
expr = "M_{a*(c*t_{R_{P*(A*(C*S))}})}"
333+
symbol_map, py_symbol_map, latex_to_py, py_to_latex = create_latex_mapping(expr)
334+
335+
assert expr in latex_to_py
336+
py_name = latex_to_py[expr]
337+
assert py_name in py_symbol_map, f"Python name '{py_name}' missing from py_symbol_map"

tests/pydasa/validations/test_patterns.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,20 @@ def test_latex_var_token_re_matches_nested_subscripts(self) -> None:
109109
for case in self.test_data["VALID_LATEX_VAR_TOKEN"]:
110110
assert pattern.search(case), f"Failed to match: {case}"
111111

112+
def test_latex_var_token_re_fullmatch_deep_nesting(self) -> None:
113+
"""Test that LATEX_VAR_TOKEN_RE captures the full token for deeply nested subscripts."""
114+
pattern = re.compile(pat.LATEX_VAR_TOKEN_RE)
115+
deep_cases = [
116+
"M_{a*(c*t_{R_{PACS}})}",
117+
"M_{a*(c*t_{R_{P*(A*(C*S))}})}",
118+
]
119+
for case in deep_cases:
120+
match = pattern.search(case)
121+
assert match is not None, f"Failed to match: {case}"
122+
assert match.group(0) == case, (
123+
f"Partial match for {case}: got '{match.group(0)}'"
124+
)
125+
112126
# Cross-Pattern Tests
113127
def test_all_patterns_exist(self) -> None:
114128
"""Test that all expected pattern variables exist."""

0 commit comments

Comments
 (0)