Skip to content

Commit b95a29e

Browse files
committed
added more docs.
1 parent 6161f75 commit b95a29e

1 file changed

Lines changed: 73 additions & 33 deletions

File tree

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,105 @@
11
# transient_analysis/routhTool/utils.py
2+
"""Utility helpers for :mod:`transient_analysis.routhTool`.
3+
4+
This module intentionally keeps parsing and formatting logic small and
5+
side-effect free so it can be reused by the CLI, app façade, and API layer.
6+
"""
7+
28
from __future__ import annotations
3-
from typing import List, Union, Sequence
9+
10+
from typing import Any, Sequence, Union
411

512
try:
613
import sympy as sp
7-
except Exception: # pragma: no cover
14+
except Exception: # pragma: no cover - SymPy is optional at import time.
815
sp = None
916

10-
def parse_coeffs(raw: str) -> List[str]:
11-
s = raw.replace(";", " ").replace("[", "").replace("]", "").replace(",", " ")
12-
return [t for t in s.split() if t.strip()]
17+
NumericOrSymbolic = Union[float, int, "sp.Basic"]
18+
19+
20+
def parse_coeffs(raw: str) -> list[str]:
21+
"""Parse a coefficient string into individual token strings.
22+
23+
Accepted separators are commas, semicolons, and whitespace. Square brackets
24+
are ignored so users can pass values such as ``[1, 5, 6, K]`` from the CLI.
25+
26+
Args:
27+
raw: Coefficients in descending polynomial order.
28+
29+
Returns:
30+
A list of non-empty coefficient tokens.
31+
"""
32+
if raw is None:
33+
raise ValueError("Coefficient text cannot be None.")
34+
35+
text = str(raw).strip()
36+
if not text:
37+
raise ValueError("Coefficient text cannot be empty.")
1338

14-
def coerce_tokens(
15-
tokens: Sequence[str], symbol_names: Sequence[str]
16-
) -> List[Union[float, int, "sp.Symbol"]]:
17-
"""Coerce a list of string tokens into floats (when possible) or SymPy Symbols for declared symbols.
39+
text = text.replace(";", " ").replace("[", " ").replace("]", " ").replace(",", " ")
40+
tokens = [token for token in text.split() if token.strip()]
41+
if not tokens:
42+
raise ValueError("No coefficient tokens were found.")
43+
return tokens
1844

19-
Design choice:
20-
- If *no* symbols are declared, we prefer **pure numeric** (floats) to keep downstream logic numeric,
21-
so Routh sign-change counting remains available.
22-
- Only when a token cannot be parsed as a float and SymPy is available do we fall back to nsimplify.
23-
- If a token is explicitly declared as a symbol, return a SymPy Symbol regardless.
45+
46+
def coerce_tokens(tokens: Sequence[str], symbol_names: Sequence[str] | None) -> list[NumericOrSymbolic]:
47+
"""Convert coefficient tokens into numeric or symbolic objects.
48+
49+
The coercion policy is deliberately conservative. Declared symbol names are
50+
converted to SymPy symbols. All other tokens are parsed as floats first. If
51+
float parsing fails and SymPy is installed, the token is parsed with
52+
``sympy.nsimplify``. This keeps numeric Routh workflows numeric while still
53+
supporting symbolic gain parameters such as ``K``.
54+
55+
Args:
56+
tokens: Tokenized coefficient values in descending polynomial order.
57+
symbol_names: Names that should be treated as symbolic parameters.
58+
59+
Returns:
60+
A list containing floats and, when SymPy is available, symbolic values.
61+
62+
Raises:
63+
ValueError: If a symbolic token is requested without SymPy, or if a
64+
non-numeric token cannot be parsed without SymPy.
2465
"""
25-
symbol_set = set(symbol_names or [])
26-
coerced = []
27-
for tok in tokens:
28-
coerced.append(_to_numeric_or_symbol(tok, symbol_set))
29-
return coerced
30-
31-
def _to_numeric_or_symbol(token: str, symbol_set: set):
32-
# If explicitly marked as a symbol, return SymPy Symbol (if available) or raise.
66+
symbol_set = {str(name).strip() for name in (symbol_names or []) if str(name).strip()}
67+
return [_to_numeric_or_symbol(str(token).strip(), symbol_set) for token in tokens]
68+
69+
70+
def _to_numeric_or_symbol(token: str, symbol_set: set[str]) -> NumericOrSymbolic:
71+
"""Coerce one token to a float, SymPy expression, or declared symbol."""
72+
if not token:
73+
raise ValueError("Encountered an empty coefficient token.")
74+
3375
if token in symbol_set:
3476
if sp is None:
35-
raise ValueError(f"Token '{token}' declared as symbol but SymPy is not installed.")
77+
raise ValueError(f"Token {token!r} was declared as a symbol, but SymPy is not installed.")
3678
return sp.Symbol(token, real=True)
3779

38-
# Prefer numeric first: keep numerics as floats when possible.
3980
try:
40-
# int-like strings become floats (consistent with downstream float usage)
4181
return float(token)
4282
except ValueError:
4383
pass
4484

45-
# Non-numeric: try SymPy if available (e.g., rational expressions like '3/5')
4685
if sp is not None:
4786
try:
4887
return sp.nsimplify(token, rational=True)
4988
except Exception:
50-
# last resort: a symbol-like token that wasn't declared; create a Symbol to avoid crash
5189
return sp.Symbol(token, real=True)
5290

53-
# No SymPy available and not numeric -> cannot coerce
54-
raise ValueError(f"Token '{token}' is not numeric and SymPy is not available to parse it.")
91+
raise ValueError(f"Token {token!r} is not numeric and SymPy is not available to parse it.")
5592

56-
def fmt_cell(x) -> str:
57-
if sp and isinstance(x, sp.Basic):
93+
94+
def fmt_cell(x: Any) -> str:
95+
"""Format a Routh-table cell for console and JSON-safe output."""
96+
if sp is not None and isinstance(x, sp.Basic):
5897
try:
5998
return str(sp.simplify(x))
60-
except Exception: # pragma: no cover
99+
except Exception: # pragma: no cover - defensive formatting fallback.
61100
return str(x)
101+
62102
try:
63103
return f"{float(x): .6g}"
64-
except Exception: # pragma: no cover
104+
except Exception: # pragma: no cover - defensive formatting fallback.
65105
return str(x)

0 commit comments

Comments
 (0)