Skip to content

Commit 7554cdd

Browse files
koubaaMohamed Koubaapyansys-ci-bot
authored
fix: parameter substitution for all card types (#983)
Co-authored-by: Mohamed Koubaa <[email protected]> Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent ea446bb commit 7554cdd

File tree

6 files changed

+641
-23
lines changed

6 files changed

+641
-23
lines changed

agents/parameters.md

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,10 @@ def test_local_parameter_isolation():
276276
deck = Deck()
277277
deck.import_file("top.k") # Has global param
278278
deck = deck.expand(recurse=True, cwd=test_dir)
279-
279+
280280
# Global param should be accessible
281281
assert deck.parameters.get("global_param") == expected_value
282-
282+
283283
# Local param from include should NOT be accessible
284284
with pytest.raises(KeyError):
285285
deck.parameters.get("local_param")
@@ -296,15 +296,135 @@ When implementing parameter-related features:
296296
- [ ] Test visibility: Verify child scopes can see parent params
297297
- [ ] Add logging: Use `logger.debug()` for parameter operations
298298

299+
## Parameter Substitution in Card Types
300+
301+
All card types now support LS-DYNA parameter substitution (`&parameter` and `-&parameter` syntax). This feature allows keywords to reference parameters defined via `*PARAMETER` or `*PARAMETER_LOCAL` keywords.
302+
303+
### Supported Card Types
304+
305+
**Card** (standard single-line cards):
306+
- Used for most keyword cards with fixed fields
307+
- Full parameter support via `load_dataline()`
308+
- Example: `*SECTION_SHELL` with `&thickness` parameter
309+
310+
**SeriesCard** (arrays/lists):
311+
- Used for curve data, material properties arrays, etc.
312+
- Parameters supported in bounded and unbounded modes
313+
- Implementation: Passes `parameter_set` through to `load_dataline()`
314+
- Example: `*DEFINE_CURVE` with parametric Y-values
315+
316+
**TableCard** (tabular data):
317+
- Used for node coordinates, element connectivity, etc.
318+
- Parameters detected automatically via `_has_parameters()`
319+
- Smart path selection: Uses fast pandas path when no parameters, falls back to `load_dataline()` when parameters detected
320+
- Implementation: `_load_lines_with_parameters()` processes line-by-line
321+
- Example: `*NODE` with parametric coordinates
322+
323+
**TableCardGroup** (multiple interleaved tables):
324+
- Used for keywords with multiple cards per row
325+
- Inherits parameter support from TableCard
326+
- Parameters work across all sub-cards in the group
327+
328+
### Parameter Name Constraints
329+
330+
LS-DYNA parameter names must fit within field widths. For a 10-character field:
331+
- `&dens` fits (5 chars)
332+
- `&density` may be truncated (8 chars)
333+
334+
Always verify parameter names fit within the field widths defined in your keyword schema.
335+
336+
### Example Usage
337+
338+
```python
339+
from ansys.dyna.core.lib.deck import Deck
340+
341+
deck_text = """*KEYWORD
342+
*PARAMETER
343+
R_dens 7850.0
344+
I_nid 1000
345+
*DEFINE_CURVE
346+
&nid
347+
0.0 0.0
348+
&dens 100.0
349+
*NODE
350+
&nid &dens 0.0 0.0
351+
*END"""
352+
353+
deck = Deck()
354+
deck.loads(deck_text)
355+
356+
# All keywords parse successfully with substituted values
357+
curve = deck.keywords[0]
358+
assert curve.lcid == 1000 # From I_nid parameter
359+
360+
node = deck.keywords[1]
361+
assert node.nodes.table["nid"][0] == 1000
362+
assert node.nodes.table["x"][0] == 7850.0
363+
```
364+
365+
### Implementation Details
366+
367+
**SeriesCard** (`src/ansys/dyna/core/lib/series_card.py`):
368+
- `read()` accepts `parameter_set` and passes to load methods
369+
- `_read_line()` passes `parameter_set` to `load_dataline()`
370+
- Works in both bounded and unbounded modes
371+
372+
**TableCard** (`src/ansys/dyna/core/lib/table_card.py`):
373+
- `_has_parameters()` detects `&` in data lines
374+
- `_load_lines_with_parameters()` uses `load_dataline()` for each row
375+
- `_load_lines()` chooses appropriate path based on parameter detection
376+
- No performance impact when parameters not present
377+
378+
**TableCardGroup** (`src/ansys/dyna/core/lib/table_card_group.py`):
379+
- Passes `parameter_set` to child `TableCard` instances
380+
- No special handling needed (inherits from TableCard)
381+
382+
### Testing
383+
384+
See `tests/test_parameter_substitution.py` for comprehensive test coverage including:
385+
- Bounded and unbounded modes for all card types
386+
- Negative parameters (`-&param`)
387+
- Type conversion and validation
388+
- Error handling for missing/mismatched parameters
389+
- Mixed parameters and literal values
390+
- Regression testing for non-parameter cases
391+
392+
### Error Handling
393+
394+
The parameter substitution system provides clear error messages:
395+
396+
- **Missing parameter**: `KeyError` with parameter name
397+
- **Type mismatch**: `TypeError` explaining the conversion failure
398+
- **No parameter set**: `ValueError` when `&` found but no `parameter_set` provided
399+
400+
## Implementation Checklist (Extended)
401+
402+
When implementing parameter-related features:
403+
404+
- [ ] Consider scope: Is this global or local?
405+
- [ ] Check context: Which deck should receive the parameter?
406+
- [ ] Verify timing: Is parameter defined before use?
407+
- [ ] Test isolation: Verify parent/sibling isolation for local params
408+
- [ ] Test visibility: Verify child scopes can see parent params
409+
- [ ] Add logging: Use `logger.debug()` for parameter operations
410+
- [ ] Card type support: Ensure parameter_set is passed through card read chains
411+
- [ ] Field width constraints: Verify parameter names fit in field widths
412+
299413
## Key Files
300414

301415
- `src/ansys/dyna/core/lib/parameters.py`: ParameterSet class and ParameterHandler
302416
- `src/ansys/dyna/core/lib/deck.py`: Deck expansion and include processing
303417
- `src/ansys/dyna/core/lib/deck_loader.py`: Keyword loading and parameter substitution
304418
- `src/ansys/dyna/core/lib/import_handler.py`: ImportContext and ImportHandler base
419+
- `src/ansys/dyna/core/lib/kwd_line_formatter.py`: `load_dataline()` with parameter substitution
420+
- `src/ansys/dyna/core/lib/card.py`: Standard card parameter support
421+
- `src/ansys/dyna/core/lib/series_card.py`: SeriesCard parameter support
422+
- `src/ansys/dyna/core/lib/table_card.py`: TableCard parameter support
423+
- `src/ansys/dyna/core/lib/table_card_group.py`: TableCardGroup parameter support
424+
- `tests/test_parameter_substitution.py`: Comprehensive parameter substitution tests
305425

306426
## Related Documentation
307427

308428
- [Codegen Guide](codegen.md): Auto-generated keyword classes
309429
- [Linked Keywords](linked_keywords.md): Keyword relationships and properties
310-
- GitHub Issue #641: PARAMETER_LOCAL scoping implementation
430+
- GitHub Issue #641: Parameter substitution for all card types

doc/.vale.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ SkippedScopes = script, style, pre, figure
1717
WordTemplate = \b(?:%s)\b
1818

1919
# Ignore autogenerated files and internal development notes
20-
IgnoredFiles = **/todo.md, source/_autosummary/airbag.rst, source/_autosummary/ale.rst, source/_autosummary/battery.rst, source/_autosummary/boundary.rst, source/_autosummary/case.rst, source/_autosummary/cese.rst, source/_autosummary/change.rst, source/_autosummary/chemistry.rst, source/_autosummary/component.rst, source/_autosummary/constrained.rst, source/_autosummary/contact.rst, source/_autosummary/control.rst, source/_autosummary/controller.rst, source/_autosummary/cosim.rst, source/_autosummary/damping.rst, source/_autosummary/database.rst, source/_autosummary/define.rst, source/_autosummary/deformable.rst, source/_autosummary/delete.rst, source/_autosummary/dualcese.rst, source/_autosummary/ef.rst, source/_autosummary/element.rst, source/_autosummary/em.rst, source/_autosummary/eos.rst, source/_autosummary/fatigue.rst, source/_autosummary/frequency.rst, source/_autosummary/icfd.rst, source/_autosummary/iga.rst, source/_autosummary/include.rst, source/_autosummary/index.rst, source/_autosummary/initial.rst, source/_autosummary/integration.rst, source/_autosummary/interface.rst, source/_autosummary/keyword.rst, source/_autosummary/load.rst, source/_autosummary/lso.rst, source/_autosummary/mat.rst, source/_autosummary/mesh.rst, source/_autosummary/module.rst, source/_autosummary/node.rst, source/_autosummary/other.rst, source/_autosummary/parameter.rst, source/_autosummary/part.rst, source/_autosummary/particle.rst, source/_autosummary/perturbation.rst, source/_autosummary/rail.rst, source/_autosummary/rigid.rst, source/_autosummary/rigidwall.rst, source/_autosummary/rve.rst, source/_autosummary/section.rst, source/_autosummary/sensor.rst, source/_autosummary/set.rst
20+
IgnoredFiles = **/todo.md, AGENTS.md, agents/**, source/_autosummary/airbag.rst, source/_autosummary/ale.rst, source/_autosummary/battery.rst, source/_autosummary/boundary.rst, source/_autosummary/case.rst, source/_autosummary/cese.rst, source/_autosummary/change.rst, source/_autosummary/chemistry.rst, source/_autosummary/component.rst, source/_autosummary/constrained.rst, source/_autosummary/contact.rst, source/_autosummary/control.rst, source/_autosummary/controller.rst, source/_autosummary/cosim.rst, source/_autosummary/damping.rst, source/_autosummary/database.rst, source/_autosummary/define.rst, source/_autosummary/deformable.rst, source/_autosummary/delete.rst, source/_autosummary/dualcese.rst, source/_autosummary/ef.rst, source/_autosummary/element.rst, source/_autosummary/em.rst, source/_autosummary/eos.rst, source/_autosummary/fatigue.rst, source/_autosummary/frequency.rst, source/_autosummary/icfd.rst, source/_autosummary/iga.rst, source/_autosummary/include.rst, source/_autosummary/index.rst, source/_autosummary/initial.rst, source/_autosummary/integration.rst, source/_autosummary/interface.rst, source/_autosummary/keyword.rst, source/_autosummary/load.rst, source/_autosummary/lso.rst, source/_autosummary/mat.rst, source/_autosummary/mesh.rst, source/_autosummary/module.rst, source/_autosummary/node.rst, source/_autosummary/other.rst, source/_autosummary/parameter.rst, source/_autosummary/part.rst, source/_autosummary/particle.rst, source/_autosummary/perturbation.rst, source/_autosummary/rail.rst, source/_autosummary/rigid.rst, source/_autosummary/rigidwall.rst, source/_autosummary/rve.rst, source/_autosummary/section.rst, source/_autosummary/sensor.rst, source/_autosummary/set.rst
2121

2222
# List of Packages to be used for our guidelines
2323
Packages = Google

doc/changelog/983.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Parameter substitution for all card types

src/ansys/dyna/core/lib/series_card.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,11 @@ def _check_null(self, value) -> bool:
259259
return self._empty_struture(value)
260260
return self._check_null_by_type(value, self._type)
261261

262-
def _read_line(self, size, line):
262+
def _read_line(self, size, line, parameter_set: ParameterSet = None):
263263
num_fields = self._num_fields()
264264
width = self._get_width()
265265
read_format = [(i * width * num_fields, width, self._type) for i in range(size)]
266-
values = load_dataline(read_format, line)
266+
values = load_dataline(read_format, line, parameter_set)
267267
if len(values) == 0:
268268
raise ValueError(f"Failed to read any values from line: {line}")
269269
last_real_index = -1
@@ -276,19 +276,19 @@ def _read_line(self, size, line):
276276
values = values[: last_real_index + 1]
277277
return values
278278

279-
def _load_bounded_from_buffer(self, buf: typing.TextIO) -> None:
279+
def _load_bounded_from_buffer(self, buf: typing.TextIO, parameter_set: ParameterSet = None) -> None:
280280
num_lines = self._num_rows()
281281
for index in range(num_lines):
282282
line, exit_loop = read_line(buf)
283283
if exit_loop:
284284
break
285285
start, end = self._get_card_range(index)
286286
size = end - start
287-
values = self._read_line(size, line)
287+
values = self._read_line(size, line, parameter_set)
288288
for j, value in zip(range(start, end), values):
289289
self[j] = value
290290

291-
def _load_unbounded_from_buffer(self, buf: typing.TextIO) -> None:
291+
def _load_unbounded_from_buffer(self, buf: typing.TextIO, parameter_set: ParameterSet = None) -> None:
292292
width = self._get_width()
293293
self._initialize_data(0)
294294
while True:
@@ -305,16 +305,15 @@ def _load_unbounded_from_buffer(self, buf: typing.TextIO) -> None:
305305
print("Trailing spaces, TODO - write a test!")
306306
line = line + " " * trailing_spaces
307307
max_amount = min(size, self._get_fields_per_card())
308-
values = self._read_line(max_amount, line)
308+
values = self._read_line(max_amount, line, parameter_set)
309309
self.extend(values)
310310

311311
def read(self, buf: typing.TextIO, parameter_set: ParameterSet = None) -> bool:
312-
# parameter sets are ignored for series cards
313312
if self.bounded:
314-
self._load_bounded_from_buffer(buf)
313+
self._load_bounded_from_buffer(buf, parameter_set)
315314
return False
316315
else:
317-
self._load_unbounded_from_buffer(buf)
316+
self._load_unbounded_from_buffer(buf, parameter_set)
318317
return True
319318

320319
def _get_lines(self, format: typing.Optional[format_type], comment: bool) -> typing.List[str]:

src/ansys/dyna/core/lib/table_card.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,9 @@ def _get_fields(self) -> typing.List[Field]:
218218
return fields
219219

220220
def _load_bounded_from_buffer(self, buf: typing.TextIO, parameter_set: ParameterSet) -> None:
221-
read_options = self._get_read_options()
222-
read_options["nrows"] = self._num_rows()
223-
df = pd.read_fwf(buf, **read_options)
224-
self._table = df
225-
self._initialized = True
221+
# For bounded cards, read all lines and use same logic as unbounded
222+
data_lines = buffer_to_lines(buf, self._num_rows())
223+
self._load_lines(data_lines, parameter_set)
226224

227225
def _load_unbounded_from_buffer(self, buf: typing.TextIO, parameter_set: ParameterSet) -> None:
228226
data_lines = buffer_to_lines(buf)
@@ -237,14 +235,44 @@ def read(self, buf: typing.TextIO, parameter_set: ParameterSet = None) -> None:
237235
self._initialized = True
238236
self._load_unbounded_from_buffer(buf, parameter_set)
239237

240-
def _load_lines(self, data_lines: typing.List[str], parameter_set: ParameterSet) -> None:
238+
def _has_parameters(self, data_lines: typing.List[str]) -> bool:
239+
"""Check if any data lines contain parameter references."""
240+
return any("&" in line for line in data_lines)
241+
242+
def _load_lines_with_parameters(self, data_lines: typing.List[str], parameter_set: ParameterSet) -> None:
243+
"""Load lines using load_dataline for parameter support.
244+
245+
This method processes each line individually using load_dataline(),
246+
which handles parameter substitution. It's used when parameters are
247+
detected in the data.
248+
"""
249+
from ansys.dyna.core.lib.kwd_line_formatter import load_dataline
250+
241251
fields = self._get_fields()
242-
buffer = io.StringIO()
243-
[(buffer.write(line), buffer.write("\n")) for line in data_lines]
244-
buffer.seek(0)
245-
self._table = self._read_buffer_as_dataframe(buffer, fields, parameter_set)
252+
format_spec = [(f.offset, f.width, f.type) for f in fields]
253+
254+
rows = []
255+
for line in data_lines:
256+
values = load_dataline(format_spec, line, parameter_set)
257+
row_dict = {field.name: value for field, value in zip(fields, values)}
258+
rows.append(row_dict)
259+
260+
self._table = pd.DataFrame(rows)
246261
self._initialized = True
247262

263+
def _load_lines(self, data_lines: typing.List[str], parameter_set: ParameterSet) -> None:
264+
# Use parameter-aware loading if parameters are present
265+
if parameter_set is not None and self._has_parameters(data_lines):
266+
self._load_lines_with_parameters(data_lines, parameter_set)
267+
else:
268+
# Use fast pandas path when no parameters present
269+
fields = self._get_fields()
270+
buffer = io.StringIO()
271+
[(buffer.write(line), buffer.write("\n")) for line in data_lines]
272+
buffer.seek(0)
273+
self._table = self._read_buffer_as_dataframe(buffer, fields, parameter_set)
274+
self._initialized = True
275+
248276
def write(
249277
self,
250278
format: typing.Optional[format_type] = None,

0 commit comments

Comments
 (0)