Repository: ansys/pydyna

Issue:
For SET_PART_LIST_GENERATE keyword all variable cards are not read
### 🔍 Before submitting the issue

- [x] I have searched among the existing issues
- [x] I am using a Python virtual environment

### 🐞 Description of the bug

Fails to read multiple variable cards in SET_PART_LIST_GENERATE, only the first card is read.

### 📝 Steps to reproduce

-

### 💻 Which operating system are you using?

Windows

### 📀 Which ANSYS version are you using?

N/A

### 🐍 Which Python version are you using?

3.12

### 📦 Installed packages

```shell
ansys-api-dyna==0.4.2
ansys-api-platform-instancemanagement==1.1.2
ansys-dpf-core==0.13.4
ansys-dyna-core==0.7.0
ansys-platform-instancemanagement==1.1.2
ansys-tools-path==0.7.1
anyio==4.8.0
appdirs==1.4.4
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==3.0.0
async-lru==2.0.4
attrs==25.1.0
babel==2.16.0
beautifulsoup4==4.12.3
bleach==6.2.0
cachetools==5.5.1
certifi==2024.12.14
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
colorama==0.4.6
comm==0.2.2
contourpy==1.3.1
cycler==0.12.1
debugpy==1.8.12
decorator==5.1.1
defusedxml==0.7.1
executing==2.2.0
fastjsonschema==2.21.1
fonttools==4.55.7
fqdn==1.5.1
google-api-core==2.24.1
google-api-python-client==2.160.0
google-auth==2.38.0
google-auth-httplib2==0.2.0
googleapis-common-protos==1.66.0
grpcio==1.70.0
h11==0.14.0
hollerith==0.6.0
httpcore==1.0.7
httplib2==0.22.0
httpx==0.28.1
idna==3.10
importlib_metadata==8.6.1
ipykernel==6.29.5
ipython==8.31.0
ipywidgets==8.1.5
isoduration==20.11.0
jedi==0.19.2
Jinja2==3.1.5
json5==0.10.0
jsonpointer==3.0.0
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
jupyter==1.1.1
jupyter-console==6.6.3
jupyter-events==0.11.0
jupyter-lsp==2.2.5
jupyter_client==8.6.3
jupyter_core==5.7.2
jupyter_server==2.15.0
jupyter_server_terminals==0.5.3
jupyterlab==4.3.4
jupyterlab_pygments==0.3.0
jupyterlab_server==2.27.3
jupyterlab_widgets==3.0.13
kiwisolver==1.4.8
MarkupSafe==3.0.2
matplotlib==3.10.0
matplotlib-inline==0.1.7
mistune==3.1.1
nbclient==0.10.2
nbconvert==7.16.6
nbformat==5.10.4
nest-asyncio==1.6.0
notebook==7.3.2
notebook_shim==0.2.4
numpy==2.2.2
overrides==7.7.0
packaging==24.2
pandas==2.2.3
pandocfilters==1.5.1
parso==0.8.4
pillow==11.1.0
platformdirs==4.3.6
pooch==1.8.2
prometheus_client==0.21.1
prompt_toolkit==3.0.50
proto-plus==1.26.0
protobuf==5.29.3
psutil==6.1.1
pure_eval==0.2.3
pyasn1==0.6.1
pyasn1_modules==0.4.1
pycparser==2.22
Pygments==2.19.1
pyparsing==3.2.1
python-dateutil==2.9.0.post0
python-json-logger==3.2.1
pytz==2024.2
pyvista==0.44.2
pywin32==308
pywinpty==2.0.14
PyYAML==6.0.2
pyzmq==26.2.0
referencing==0.36.2
requests==2.32.3
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rpds-py==0.22.3
rsa==4.9
scooby==0.10.0
Send2Trash==1.8.3
setuptools==75.8.0
six==1.17.0
sniffio==1.3.1
soupsieve==2.6
stack-data==0.6.3
terminado==0.18.1
tinycss2==1.4.0
tornado==6.4.2
tqdm==4.67.1
traitlets==5.14.3
types-python-dateutil==2.9.0.20241206
typing_extensions==4.12.2
tzdata==2025.1
uri-template==1.3.0
uritemplate==4.1.1
urllib3==2.3.0
vtk==9.3.1
wcwidth==0.2.13
webcolors==24.11.1
webencodings==0.5.1
websocket-client==1.8.0
wheel==0.45.1
widgetsnbextension==4.0.13
zipp==3.21.0
```

Discussion:
@kanthadya: @MiteshLalwala, can you please share an example DYNA deck which has the SET_PART_LIST_GENERATE card in it?@MiteshLalwala: @kanthadya - Here is an example deck.
[SET_PART_GENERATE_example.txt](https://github.com/user-attachments/files/18672470/SET_PART_GENERATE_example.txt)

 In this deck the original SET_PART_LIST_GENERATE card is as below:
*SET_PART_LIST_GENERATE
         3       0.0       0.0       0.0       0.0
   2000000   2000025   2000100   2000200   2000500   2000600
   2000700   2000800   2000900   2001000
$

However, when you read the deck using PyDyna, it reads the card as below, missing the second variable card.
*SET_PART_LIST_GENERATE
$#     sid       da1       da2       da3       da4    solver
         3       0.0       0.0       0.0       0.0          
$#   b1beg     b1end     b2beg     b2end     b3beg     b3end     b4beg     b4end
   2000000   2000025   2000100   2000200   2000500   2000600   



Solution patch:
diff --git a/codegen/generate.py b/codegen/generate.py
index b160031a..2482a7e2 100644
--- a/codegen/generate.py
+++ b/codegen/generate.py
@@ -215,8 +215,21 @@ def handle_insert_cards(kwd_data, settings):
 
 def handle_variable_cards(kwd_data, settings):
     kwd_data["variable"] = True
+    dataclasses = []
     for card_settings in settings:
-        variable_card = kwd_data["cards"][card_settings["index"]]
+        card_index = card_settings["index"]
+        type_name = card_settings["type"]
+        variable_card = kwd_data["cards"][card_index]
+        if type_name == "struct":
+            struct_info = card_settings["struct-info"]
+            struct_name = struct_info["name"]
+            dataclass = {
+                "name": struct_name,
+                "fields": struct_info["fields"]
+            }
+            dataclasses.append(dataclass)
+            type_name = f"self.{struct_name}"
+
         # use abbreviations for some fields to make the jinja template more concise
         variable_card["variable"] = {
             "name": card_settings["name"],
@@ -224,9 +237,11 @@ def handle_variable_cards(kwd_data, settings):
             "width": card_settings["element-width"],
             "length_func": card_settings.get("length-func", ""),
             "active_func": card_settings.get("active-func", ""),
-            "type": card_settings["type"],
+            "type": type_name,
             "help": card_settings["help"],
         }
+    if len(dataclasses) > 0:
+        kwd_data["dataclasses"] = dataclasses
 
 
 def handle_conditional_cards(kwd_data, settings):
diff --git a/codegen/manifest.json b/codegen/manifest.json
index 8c6e202d..1db59ab5 100644
--- a/codegen/manifest.json
+++ b/codegen/manifest.json
@@ -1762,6 +1762,27 @@
             ]
         }
     },
+    "SET_PART_LIST_GENERATE": {
+        "generation-options": {
+            "variable-card": [
+                {
+                    "index": 1,
+                    "name": "block_ranges",
+                    "card-size": 8,
+                    "element-width": 10,
+                    "type": "struct",
+                    "struct-info": {
+                        "name": "BlockRange",
+                        "fields": [
+                            {"name": "bbeg", "type": "int"},
+                            {"name": "bend", "type": "int"}
+                        ]
+                    },
+                    "help": "Block ranges."
+                }
+            ]
+        }
+    },
     "SET_BEAM": {
         "generation-options": {
             "variable-card": [
diff --git a/codegen/templates/keyword.j2 b/codegen/templates/keyword.j2
index 7ae425e7..7ce84398 100644
--- a/codegen/templates/keyword.j2
+++ b/codegen/templates/keyword.j2
@@ -1,4 +1,7 @@
 {{license}}
+{% if keyword_data.dataclasses %}
+import dataclasses
+{% endif %}
 import typing
 {# TODO - only do this if there's a keyword without a default #}
 {% if keyword_data.mixin_imports %}
@@ -100,6 +103,14 @@ class {{keyword_data.classname}}(KeywordBase):
 {% endfor %}
     ]
 {% endif %}
+{% for dataclass in keyword_data.dataclasses %}
+
+    @dataclasses.dataclass
+    class {{ dataclass.name }}:
+{% for field in dataclass.fields %}
+        {{ field.name }}: {{field.type}} = None
+{% endfor %}
+{% endfor %}
 
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
diff --git a/doc/changelog/702.documentation.md b/doc/changelog/702.documentation.md
new file mode 100644
index 00000000..8cb39f22
--- /dev/null
+++ b/doc/changelog/702.documentation.md
@@ -0,0 +1 @@
+fix: SET_PART_LIST_GENERATE
\ No newline at end of file
diff --git a/src/ansys/dyna/core/keywords/keyword_classes/auto/set_part_list_generate.py b/src/ansys/dyna/core/keywords/keyword_classes/auto/set_part_list_generate.py
index 633b7e94..0f58ba4b 100644
--- a/src/ansys/dyna/core/keywords/keyword_classes/auto/set_part_list_generate.py
+++ b/src/ansys/dyna/core/keywords/keyword_classes/auto/set_part_list_generate.py
@@ -20,9 +20,11 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
+import dataclasses
 import typing
 from ansys.dyna.core.lib.card import Card, Field, Flag
 from ansys.dyna.core.lib.config import use_lspp_defaults
+from ansys.dyna.core.lib.variable_card import VariableCard
 from ansys.dyna.core.lib.option_card import OptionCardSet, OptionSpec
 from ansys.dyna.core.lib.keyword_base import KeywordBase
 
@@ -35,6 +37,11 @@ class SetPartListGenerate(KeywordBase):
         OptionSpec("TITLE", -1, 1),
     ]
 
+    @dataclasses.dataclass
+    class BlockRange:
+        bbeg: int = None
+        bend: int = None
+
     def __init__(self, **kwargs):
         super().__init__(**kwargs)
         kwargs["parent"] = self
@@ -85,66 +92,13 @@ class SetPartListGenerate(KeywordBase):
                     ),
                 ],
             ),
-            Card(
-                [
-                    Field(
-                        "b1beg",
-                        int,
-                        0,
-                        10,
-                        kwargs.get("b1beg")
-                    ),
-                    Field(
-                        "b1end",
-                        int,
-                        10,
-                        10,
-                        kwargs.get("b1end")
-                    ),
-                    Field(
-                        "b2beg",
-                        int,
-                        20,
-                        10,
-                        kwargs.get("b2beg")
-                    ),
-                    Field(
-                        "b2end",
-                        int,
-                        30,
-                        10,
-                        kwargs.get("b2end")
-                    ),
-                    Field(
-                        "b3beg",
-                        int,
-                        40,
-                        10,
-                        kwargs.get("b3beg")
-                    ),
-                    Field(
-                        "b3end",
-                        int,
-                        50,
-                        10,
-                        kwargs.get("b3end")
-                    ),
-                    Field(
-                        "b4beg",
-                        int,
-                        60,
-                        10,
-                        kwargs.get("b4beg")
-                    ),
-                    Field(
-                        "b4end",
-                        int,
-                        70,
-                        10,
-                        kwargs.get("b4end")
-                    ),
-                ],
-            ),
+            VariableCard(
+                "block_ranges",
+                8,
+                10,
+                self.BlockRange,
+                None,
+                data = kwargs.get("block_ranges")),
             OptionCardSet(
                 option_spec = SetPartListGenerate.option_specs[0],
                 cards = [
@@ -229,84 +183,9 @@ class SetPartListGenerate(KeywordBase):
         self._cards[0].set_value("solver", value)
 
     @property
-    def b1beg(self) -> typing.Optional[int]:
-        """Get or set the First part ID in the first block.
-        """ # nopep8
-        return self._cards[1].get_value("b1beg")
-
-    @b1beg.setter
-    def b1beg(self, value: int) -> None:
-        self._cards[1].set_value("b1beg", value)
-
-    @property
-    def b1end(self) -> typing.Optional[int]:
-        """Get or set the Last part ID in the first block.
-        """ # nopep8
-        return self._cards[1].get_value("b1end")
-
-    @b1end.setter
-    def b1end(self, value: int) -> None:
-        self._cards[1].set_value("b1end", value)
-
-    @property
-    def b2beg(self) -> typing.Optional[int]:
-        """Get or set the First part ID in the second block.
-        """ # nopep8
-        return self._cards[1].get_value("b2beg")
-
-    @b2beg.setter
-    def b2beg(self, value: int) -> None:
-        self._cards[1].set_value("b2beg", value)
-
-    @property
-    def b2end(self) -> typing.Optional[int]:
-        """Get or set the Last part ID in the second block.
-        """ # nopep8
-        return self._cards[1].get_value("b2end")
-
-    @b2end.setter
-    def b2end(self, value: int) -> None:
-        self._cards[1].set_value("b2end", value)
-
-    @property
-    def b3beg(self) -> typing.Optional[int]:
-        """Get or set the First part ID in the third block.
-        """ # nopep8
-        return self._cards[1].get_value("b3beg")
-
-    @b3beg.setter
-    def b3beg(self, value: int) -> None:
-        self._cards[1].set_value("b3beg", value)
-
-    @property
-    def b3end(self) -> typing.Optional[int]:
-        """Get or set the Last part ID in the third block.
-        """ # nopep8
-        return self._cards[1].get_value("b3end")
-
-    @b3end.setter
-    def b3end(self, value: int) -> None:
-        self._cards[1].set_value("b3end", value)
-
-    @property
-    def b4beg(self) -> typing.Optional[int]:
-        """Get or set the First part ID in the fourth block.
-        """ # nopep8
-        return self._cards[1].get_value("b4beg")
-
-    @b4beg.setter
-    def b4beg(self, value: int) -> None:
-        self._cards[1].set_value("b4beg", value)
-
-    @property
-    def b4end(self) -> typing.Optional[int]:
-        """Get or set the Last part ID in the fourth block.
-        """ # nopep8
-        return self._cards[1].get_value("b4end")
-
-    @b4end.setter
-    def b4end(self, value: int) -> None:
-        self._cards[1].set_value("b4end", value)
+    def block_ranges(self) -> VariableCard:
+        """Block ranges."""
+        return self._cards[1]
 
     @property
     def title(self) -> typing.Optional[str]:
diff --git a/src/ansys/dyna/core/lib/field.py b/src/ansys/dyna/core/lib/field.py
index 1bbf5444..fc83f1ee 100644
--- a/src/ansys/dyna/core/lib/field.py
+++ b/src/ansys/dyna/core/lib/field.py
@@ -24,6 +24,8 @@ import copy
 import dataclasses
 import typing
 
+import pandas as pd
+
 
 @dataclasses.dataclass
 class Flag:
@@ -40,6 +42,9 @@ class Field:
         self._width = width
         self._value = value
 
+    def __repr__(self) -> str:
+        return f"Field({self.name}, {self.type}, {self.offset}, {self.width}, {self.value})"
+
     @property
     def name(self) -> str:
         return self._name
@@ -74,20 +79,30 @@ class Field:
 
     @property
     def value(self) -> typing.Any:
-        if self._value and type(self._value) == Flag:
+        if self._truthy_value(self._value) and type(self._value) == Flag:
             return self._value.value
         return self._value
 
+    def _truthy_value(self, value) -> bool:
+        if pd.isna(value):
+            return False
+        return True if value else False
+
+    def _no_value_flag(self, value) -> bool:
+        return self._truthy_value(value) and type(value) is Flag
+
     @value.setter
     def value(self, value: typing.Any) -> None:
-        if self._value and type(self._value) == Flag:
+        no_value_flag = self._no_value_flag(value)
+        if no_value_flag:
             self._value.value = value
         else:
             self._value = value
 
     def io_info(self) -> typing.Tuple[str, typing.Type]:
         """Return the value and type used for io."""
-        if self._value and type(self._value) == Flag:
+        no_value_flag = self._no_value_flag(self._value)
+        if no_value_flag:
             value = self._value.true_value if self._value.value else self._value.false_value
             return value, str
         return self.value, self.type
diff --git a/src/ansys/dyna/core/lib/field_writer.py b/src/ansys/dyna/core/lib/field_writer.py
index f0b4a0fd..5019b367 100644
--- a/src/ansys/dyna/core/lib/field_writer.py
+++ b/src/ansys/dyna/core/lib/field_writer.py
@@ -20,6 +20,8 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
+import copy
+import dataclasses
 import io
 import typing
 
@@ -62,8 +64,21 @@ def _field_iterator(fields: typing.List[Field], long_format: bool) -> typing.Ite
             yield Field(name=None, type=None, offset=offset, width=empty_width)
             offset += empty_width
         assert pos == offset
-        yield field
-        offset += width
+        if dataclasses.is_dataclass(field.type):
+            field_offset = field.offset
+            total_width = 0
+            for member in dataclasses.fields(field.type):
+                if field.value != None:
+                    value = getattr(field.value, member.name)
+                else:
+                    value = None
+                yield Field(name=member.name, type=member.type, offset=field_offset, width=field.width, value=value)
+                total_width += field.width
+                field_offset += field.width
+            offset += total_width
+        else:
+            yield field
+            offset += width
 
 
 def check_field_type(field_type: type):
@@ -146,16 +161,16 @@ def write_fields(
     >>> s.getvalue()
     '         1     hello'
     """
-    index = 0
+    if values != None:
+        fields = copy.deepcopy(fields)
+        for field, value in zip(fields, values):
+            field.value = value
 
     for field in _field_iterator(fields, format == format_type.long):
         if field.type is None:
             buf.write(_write_null(field.width))
         else:
             field_value, field_type = field.io_info()
-            if values != None:
-                field_value = values[index]
-                index += 1
             write_field(buf, field_type, field_value, field.width)
 
 
diff --git a/src/ansys/dyna/core/lib/kwd_line_formatter.py b/src/ansys/dyna/core/lib/kwd_line_formatter.py
index e780900d..b31e0b64 100644
--- a/src/ansys/dyna/core/lib/kwd_line_formatter.py
+++ b/src/ansys/dyna/core/lib/kwd_line_formatter.py
@@ -20,6 +20,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
+import dataclasses
 import typing
 import warnings
 
@@ -82,7 +83,37 @@ def buffer_to_lines(buf: typing.TextIO, max_num_lines: int = -1) -> typing.List[
     return data_lines
 
 
-def load_dataline(spec: typing.List[tuple], line_data: str, parameter_set: ParameterSet = None) -> tuple:
+def _expand_spec(spec: typing.List[tuple]) -> typing.List[tuple]:
+    specs = []
+    for item in spec:
+        position, width, item_type = item
+        if dataclasses.is_dataclass(item_type):
+            offset = position
+            for field in dataclasses.fields(item_type):
+                item_spec = (offset, width, field.type)
+                offset = offset + width
+                specs.append(item_spec)
+        else:
+            specs.append(item)
+    return specs
+
+
+def _contract_data(spec: typing.List[tuple], data: typing.List) -> typing.Iterable:
+    iterspec = iter(spec)
+    iterdata = iter(data)
+    while True:
+        try:
+            _, _, item_type = next(iterspec)
+            if dataclasses.is_dataclass(item_type):
+                args = [next(iterdata) for f in dataclasses.fields(item_type)]
+                yield item_type(*args)
+            else:
+                yield next(iterdata)
+        except StopIteration:
+            return
+
+
+def load_dataline(spec: typing.List[tuple], line_data: str, parameter_set: ParameterSet = None) -> typing.List:
     """loads a keyword card line with fixed column offsets and width from string
     spec: list of tuples representing the (offset, width, type) of each field
     line_data: string with keyword data
@@ -126,9 +157,10 @@ def load_dataline(spec: typing.List[tuple], line_data: str, parameter_set: Param
         assert type(value) == item_type
         return value
 
+    expanded_spec = _expand_spec(spec)
     data = []
     end_position = 0
-    for item_spec in spec:
+    for item_spec in expanded_spec:
         position, width, item_type = item_spec
         end_position, text_block = seek_text_block(line_data, position, width)
         if not has_value(text_block):
@@ -150,4 +182,5 @@ def load_dataline(spec: typing.List[tuple], line_data: str, parameter_set: Param
         warning_message = f'Detected out of bound card characters:\n"{line_data[end_position:]}"\n"Ignoring.'
         warnings.warn(warning_message)
 
+    data = list(_contract_data(spec, data))
     return tuple(data)
diff --git a/src/ansys/dyna/core/lib/variable_card.py b/src/ansys/dyna/core/lib/variable_card.py
index 927d7f37..977b2eb5 100644
--- a/src/ansys/dyna/core/lib/variable_card.py
+++ b/src/ansys/dyna/core/lib/variable_card.py
@@ -20,6 +20,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
+import dataclasses
 import io
 import math
 import typing
@@ -27,7 +28,7 @@ import typing
 import ansys.dyna.core.lib.array as arr
 from ansys.dyna.core.lib.card_interface import CardInterface
 from ansys.dyna.core.lib.field import Field
-from ansys.dyna.core.lib.field_writer import write_fields
+from ansys.dyna.core.lib.field_writer import write_comment_line, write_fields
 from ansys.dyna.core.lib.format_type import format_type
 from ansys.dyna.core.lib.kwd_line_formatter import load_dataline, read_line
 from ansys.dyna.core.lib.parameter_set import ParameterSet
@@ -39,19 +40,23 @@ class VariableCard(CardInterface):
     def __init__(
         self,
         name: str,
-        card_size: int,
+        fields_per_card: int,
         element_width: int,
-        type: type,
+        input_type: typing.Union[type, typing.List[type]],
         length_func: typing.Callable = None,
         active_fn: typing.Callable = None,
+        type_names: typing.Optional[typing.List[str]] = None,
         data=None,
         format: format_type = format_type.default,
     ):
         self._name = name
-        self._card_size = card_size
+        self._fields_per_card = fields_per_card
         self._element_width = element_width
         self._active_func = active_fn
-        self._type = type
+        if isinstance(input_type, list):
+            self._type = self._make_struct_datatype(type_names, input_type)
+        else:
+            self._type = input_type
         self._initialize_data(0)
         if isinstance(data, list):
             self.data = data
@@ -63,6 +68,17 @@ class VariableCard(CardInterface):
             self._length_func = length_func
         self._format_type = format
 
+    def _make_struct_datatype(self, type_names, type_types):
+        assert type_names is not None, "Type names are required if a list of types is used!"
+        assert len(type_types) == len(type_names), "The `type_names` and `input_type` lists have to be the same length"
+        dataclass_spec = []
+        for type_name, type_type in zip(type_names, type_types):
+            dataclass_spec.append((type_name, type_type))
+        return dataclasses.make_dataclass(self._name, dataclass_spec)
+
+    def __iter__(self) -> typing.Iterable:
+        return iter(self._data)
+
     @property
     def format(self) -> format_type:
         return self._format_type
@@ -71,34 +87,56 @@ class VariableCard(CardInterface):
     def format(self, value: format_type) -> None:
         self._format_type = value
 
-    def _initialize_data(self, length):
+    def _uses_structure(self):
+        return dataclasses.is_dataclass(self._type)
+
+    def _initialize_data(self, length: int) -> None:
         self._data = arr.array(self._type, length)
 
-    def _is_last_card(self, card_index: int):
-        count = self._length_func()
-        start = card_index * self._card_size
-        end = start + self._card_size
-        return end >= count
+    def _get_element_width(self) -> int:
+        return self._element_width * self._num_fields()
 
-    def _get_card_range(self, card_index: int):
-        start_index = self._card_size * card_index
+    def _get_fields_per_card(self) -> int:
+        num = self._fields_per_card / self._num_fields()
+        int_num = int(num)
+        assert num - int_num == 0, "Error"
+        return int_num
+
+    def _is_last_card(self, card_index: int) -> bool:
+        count = self._length_func()
+        fields_per_card = self._get_fields_per_card()
+        start = card_index * fields_per_card
+        end = start + fields_per_card
+        result = end >= count
+        return result
+
+    def _get_card_range(self, card_index: int) -> typing.Tuple[int, int]:
+        fields_per_card = self._get_fields_per_card()
+        start_index = fields_per_card * card_index
         if self._is_last_card(card_index):
             # last card, only use the remainder for number of fields
-            remainder = self._length_func() % self._card_size
+            remainder = self._length_func() % fields_per_card
             if remainder == 0:
-                remainder = self._card_size
+                remainder = fields_per_card
             end_index = start_index + remainder
         else:
             # not last card, use all fields
-            end_index = start_index + self._card_size
+            end_index = start_index + fields_per_card
         return start_index, end_index
 
-    def _get_comment(self, format: format_type) -> str:
-        if not self._is_last_card(0):
-            count = self._card_size
-        else:
-            # TODO test case when amount == card_size
-            count = self._length_func() % self._card_size or self._card_size
+    def _get_comment_struct(self, count: int, format: format_type) -> str:
+        s = io.StringIO()
+        comment_fields = []
+        width = self._get_width(format)
+        offset = 0
+        for _ in range(count):
+            for field in dataclasses.fields(self._type):
+                comment_fields.append(Field(field.name, field.type, offset, width))
+                offset += width
+        write_comment_line(s, comment_fields, format)
+        return s.getvalue()
+
+    def _get_comment_scalar(self, count: int, format: format_type) -> str:
         element_width = self._get_width(format)
         element = " " * element_width
         assert len(self._name) <= element_width - 2, "name of variable field is too long"
@@ -106,10 +144,26 @@ class VariableCard(CardInterface):
         array = element * count
         return "$#" + array[2:]
 
+    def _get_comment(self, format: format_type) -> str:
+        fields_per_card = self._get_fields_per_card()
+        if not self._is_last_card(0):
+            count = fields_per_card
+        else:
+            # TODO test case when amount == card_size
+            count = self._length_func() % fields_per_card or fields_per_card
+
+        if dataclasses.is_dataclass(self._type):
+            return self._get_comment_struct(count, format)
+        else:
+            return self._get_comment_scalar(count, format)
+
     def __get_value(self, index: int):
         if index < len(self._data):
             return self._data[index]
-        return self.__get_null_value()
+        if self._bounded:
+            return self.__get_null_value()
+        else:
+            raise IndexError(f"Accessing index {index} out of bound")
 
     # TODO this should be on an Array class
     def __get_null_value(self):
@@ -123,7 +177,8 @@ class VariableCard(CardInterface):
         return self._active_func()
 
     def _num_rows(self):
-        return math.ceil(self._length_func() / self._card_size)
+        fields_per_card = self._get_fields_per_card()
+        return math.ceil(self._length_func() / fields_per_card)
 
     def __getitem__(self, index):
         err_string = f"get indexer for VariableCard must be of the form [index] or [start:end].  End must be greater than start"  # noqa : E501
@@ -138,18 +193,34 @@ class VariableCard(CardInterface):
         assert end > start, err_string
         return [self.__get_value(i) for i in range(start, end)]
 
+    def _wrap_value(self, value):
+        if isinstance(value, self._type):
+            return value
+        else:
+            if self._uses_structure():
+                # allow setting struct with tuple or list
+                return self._type(*value)
+            else:
+                return self._type(value)
+
     def __setitem__(self, index: int, value):
         # first resize up to index.  TODO this should be on an Array class
         while len(self._data) <= index:
             self._data.append(self.__get_null_value())
         # then set value at the specified index
-        self._data[index] = value
+        self._data[index] = self._wrap_value(value)
 
     def append(self, value) -> None:
-        self._data.append(value)
+        self._data.append(self._wrap_value(value))
+
+    def extend(self, valuelist: typing.Iterable) -> None:
+        values = [self._wrap_value(value) for value in valuelist]
+        self._data.extend(values)
 
-    def extend(self, valuelist) -> None:
-        self._data.extend(valuelist)
+    def _num_fields(self):
+        if dataclasses.is_dataclass(self._type):
+            return len(dataclasses.fields(self._type))
+        return 1
 
     def _get_width(self, format: typing.Optional[format_type] = None):
         if format == None:
@@ -159,8 +230,14 @@ class VariableCard(CardInterface):
             width = 20
         return width
 
-    def _load_bounded_from_buffer(self, buf: typing.TextIO) -> None:
+    def _read_line(self, size, line):
+        num_fields = self._num_fields()
         width = self._get_width()
+        read_format = [(i * width * num_fields, width, self._type) for i in range(size)]
+        values = load_dataline(read_format, line)
+        return values
+
+    def _load_bounded_from_buffer(self, buf: typing.TextIO) -> None:
         num_lines = self._num_rows()
         for index in range(num_lines):
             line, exit_loop = read_line(buf)
@@ -168,8 +245,7 @@ class VariableCard(CardInterface):
                 break
             start, end = self._get_card_range(index)
             size = end - start
-            read_format = [(i * width, width, self._type) for i in range(size)]
-            values = load_dataline(read_format, line)
+            values = self._read_line(size, line)
             for j, value in zip(range(start, end), values):
                 self[j] = value
 
@@ -182,13 +258,15 @@ class VariableCard(CardInterface):
                 break
             # this is going to be slower... because we don't know how many
             # lines there are going to be.
-            size = math.ceil(len(line) / width)
-            trailing_spaces = len(line) - width * size
+            num_fields = self._num_fields()
+            text_width = width * num_fields
+            size = math.ceil(len(line) / (text_width))
+            trailing_spaces = len(line) - text_width * size
             if trailing_spaces > 0:
                 print("Trailing spaces, TODO - write a test!")
                 line = line + " " * trailing_spaces
-            read_format = [(i * width, width, self._type) for i in range(size)]
-            values = load_dataline(read_format, line)
+            max_amount = min(size, self._get_fields_per_card())
+            values = self._read_line(max_amount, line)
             self.extend(values)
 
     def read(self, buf: typing.TextIO, parameter_set: ParameterSet = None) -> bool:
@@ -233,9 +311,13 @@ class VariableCard(CardInterface):
         but its an easy way to reuse the code in write_fields so we create fields
         on the fly here. TODO - reuse less of the code without creating fields on the fly"""
         row_values = self[start_index:end_index]
-        width = self._element_width
+        field_width = self._get_element_width()
+        element_width = self._element_width
         size = end_index - start_index
-        row_fields = [Field(self._name, self._type, i * width, width) for i in range(size)]
+
+        # the field writer uses the element width, not the struct width, for structured types
+        # the offset, however, takes into account the struct width.
+        row_fields = [Field(self._name, self._type, i * field_width, element_width) for i in range(size)]
         s = io.StringIO()
         write_fields(s, row_fields, row_values, format)
         return s.getvalue()
@@ -266,7 +348,4 @@ class VariableCard(CardInterface):
     @data.setter
     def data(self, vallist: typing.List) -> None:
         self._initialize_data(0)
-        self._data.extend(vallist)
-
-    def extend(self, values: typing.List) -> None:
-        self._data.extend(values)
+        self.extend(vallist)


Test patch:
diff --git a/tests/test_deck.py b/tests/test_deck.py
index e1417ba8..fc1a5f63 100644
--- a/tests/test_deck.py
+++ b/tests/test_deck.py
@@ -357,6 +357,8 @@ def test_deck_expand_recursive_include_path(file_utils):
     deck.append(kwd.IncludePath(path=include_path2))
     deck.append(kwd.Include(filename='bird_B.k'))
     deck = deck.expand(recurse=True)
+    print(deck)
+    assert len(deck.all_keywords) == 40
     assert len(deck.keywords) == 36
 
 @pytest.mark.keywords
diff --git a/tests/test_field_writer.py b/tests/test_field_writer.py
index 62928b2e..42e0e1e2 100644
--- a/tests/test_field_writer.py
+++ b/tests/test_field_writer.py
@@ -20,7 +20,9 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
+import dataclasses
 import io
+import math
 import typing
 
 from ansys.dyna.core.lib.field import Field
@@ -30,6 +32,12 @@ from ansys.dyna.core.lib.format_type import format_type
 
 import pytest
 
+
+@dataclasses.dataclass
+class bi:
+    foo: float = None
+    bar: float = None
+
 def _get_comment_line(fields: typing.List[Field]) -> str:
     s = io.StringIO()
     field_writer.write_comment_line(s, fields)
@@ -46,10 +54,17 @@ def _get_field_value(fields: typing.List[Field], long: bool = False) -> str:
 
 @pytest.mark.keywords
 def test_comment():
+    """Test writing the comment line."""
     fields = [Field("a", int, 0, 10, 1), Field("b", str, 10, 10, "hello")]
     result = _get_comment_line(fields)
     assert result == "$#       a         b"
 
+@pytest.mark.keywords
+def test_comment_struct():
+    """Test writing the comment line for a struct type."""
+    fields = [Field("a", bi, 0, 10, None), Field("a", bi, 20, 10, None)]
+    result = _get_comment_line(fields)
+    assert result == "$#     foo       bar       foo       bar"
 
 @pytest.mark.keywords
 def test_comment_with_gap():
@@ -85,6 +100,15 @@ def test_field_values_int_string():
     result = _get_field_value(fields)
     assert result == "         1hello     "
 
+@pytest.mark.keywords
+def test_field_values_struct_single():
+    """Test writing fields with a single struct value type"""
+
+    # single value
+    bi1 = bi(1.0, 2.0)
+    fields = [Field("a", bi, 0, 10, bi1)]
+    result = _get_field_value(fields)
+    assert result == "       1.0       2.0"
 
 @pytest.mark.keywords
 def test_field_values_int_string_gap():
@@ -107,6 +131,14 @@ def test_field_values_with_nan():
     result = _get_field_value(fields)
     assert result == "         1          hello     "
 
+@pytest.mark.keywords
+def test_field_write_struct_multiple_missing_value():
+    """Test writing fields with multiple values of struct type with a missing value."""
+    bi1, bi2 = bi(1.0, 2.0), bi(1.3, math.nan)
+    fields = [Field("a", bi, 0, 10, bi1), Field("a", bi, 20, 10, bi2)]
+    result = _get_field_value(fields)
+    assert result == "       1.0       2.0       1.3          "
+
 
 @pytest.mark.keywords
 def test_field_overriden_values():
@@ -134,3 +166,29 @@ def test_field_values_int_string_long():
     fields = [Field("a", int, 0, 10, 1), Field("b", str, 10, 10, "hello")]
     result = _get_field_value(fields, True)
     assert result == "                   1hello               "
+
+@pytest.mark.keywords
+def test_field_write_struct_multiple():
+    """Test writing fields with multiple values of struct type."""
+    bi1, bi2 = bi(1.0, 2.0), bi(1.3, 2.9)
+    fields = [Field("a", bi, 0, 10, bi1), Field("a", bi, 20, 10, bi2)]
+    result = _get_field_value(fields)
+    assert result == "       1.0       2.0       1.3       2.9"
+
+@pytest.mark.keywords
+def test_field_write_struct_overridden_value():
+    #overriden struct values
+    s = io.StringIO()
+    values = [bi(1.8, math.nan), bi(0.1, 3.14)]
+    fields = [Field("a", bi, 0, 10, None), Field("a", bi, 20, 10, None)]
+    field_writer.write_fields(s, fields, values)
+    result = s.getvalue()
+    assert result == "       1.8                 0.1      3.14"
+
+
+@pytest.mark.keywords
+def test_field_write_struct_comment():
+    # comment line
+    fields = [Field("a", bi, 0, 10, None), Field("a", bi, 20, 10, None)]
+    result = _get_comment_line(fields)
+    assert result == "$#     foo       bar       foo       bar"
diff --git a/tests/test_keywords.py b/tests/test_keywords.py
index ed096105..a075949a 100644
--- a/tests/test_keywords.py
+++ b/tests/test_keywords.py
@@ -753,6 +753,14 @@ def test_set_node_title(ref_string):
     s.loads(ref_string.test_set_node_title)
 
 
+@pytest.mark.keywords
+def test_set_node_list(ref_string):
+    s = kwd.SetNodeList()
+    with pytest.warns(UserWarning, match="Detected out of bound card characters"):
+        s.loads(ref_string.test_set_node_list)
+    assert len(s.nodes) == 8
+
+
 @pytest.mark.keywords
 def test_contact_1d(ref_string):
     c = kwd.Contact1D()
@@ -823,6 +831,39 @@ def test_set_part_list(ref_string):
     assert s.write() == ref
 
 
+@pytest.mark.keywords
+def test_set_part_list_generate(ref_string):
+    s = kwd.SetPartListGenerate()
+    s.loads(ref_string.test_set_part_list_generate_ref1)
+    assert len(s.block_ranges) == 3
+    assert s.block_ranges[1].bend == 2000200
+
+    s.loads(ref_string.test_set_part_list_generate_ref2)
+    assert len(s.block_ranges) == 5
+    assert s.block_ranges[3].bbeg == 2000700
+
+    s.block_ranges.append((10,12))
+    assert len(s.block_ranges) == 6
+    assert s.block_ranges[5].bbeg == 10
+
+    s.block_ranges.data = [(0,2), [2,3]]
+    assert len(s.block_ranges) == 2
+    assert s.block_ranges[0] == kwd.SetPartListGenerate.BlockRange(0,2)
+    with pytest.raises(TypeError):
+        s.block_ranges.append((0,3,2))
+
+    s.block_ranges.append([200])
+    assert len(s.block_ranges) == 3
+    with pytest.raises(IndexError):
+        _ = s.block_ranges[4]
+    assert s.block_ranges[2].bbeg == 200
+    assert pd.isna(s.block_ranges[2].bend)
+
+    s.block_ranges[2].bend = 201
+    for block_range in s.block_ranges:
+        assert block_range.bbeg < block_range.bend
+
+
 @pytest.mark.keywords
 def test_contact_tied_shell_edge_to_surface_id(ref_string):
     """Test reading CONTACT_TIED_SHELL_EDGE_TO_SURFACE_ID."""
diff --git a/tests/test_load_dataline.py b/tests/test_load_dataline.py
index bc5c0f62..9cabcd5b 100644
--- a/tests/test_load_dataline.py
+++ b/tests/test_load_dataline.py
@@ -20,6 +20,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
+import dataclasses
 import math
 
 import pytest
@@ -27,6 +28,11 @@ import pytest
 from ansys.dyna.core.lib.kwd_line_formatter import load_dataline
 from ansys.dyna.core.lib.parameter_set import ParameterSet
 
+@dataclasses.dataclass
+class bi:
+    foo: float = None
+    bar: float = None
+
 @pytest.mark.keywords
 def test_load_dataline_001():
     """test loading a line of right-justified int and string"""
@@ -79,3 +85,37 @@ def test_load_dataline_007():
     parameter_set.add("vdct", 1.12)
     res = load_dataline(spec, dataline, parameter_set)
     assert res[4] == 1.12
+
+@pytest.mark.keywords
+def test_load_dataline_008():
+    """Test loading a data line with a struct type."""
+    spec = [(0, 10, bi)]
+    dataline = "         2        50"
+    res = load_dataline(spec, dataline)
+    assert len(res) == 1
+    assert res[0].foo == 2
+    assert res[0].bar == 50
+
+@pytest.mark.keywords
+def test_load_dataline_009():
+    """Test loading a data line with two struct types."""
+    dataline = "         2        50         1       3.1"
+    spec = [(0, 10, bi), (20, 10, bi)]
+    res = load_dataline(spec, dataline)
+    assert len(res) == 2
+    assert res[0].foo == 2
+    assert res[0].bar == 50
+    assert res[1].foo == 1
+    assert res[1].bar == 3.1
+
+@pytest.mark.keywords
+def test_load_dataline_010():
+    """Test loading a data line with two struct types and a missing value."""
+    dataline = "         2        50         1"
+    spec = [(0, 10, bi), (20, 10, bi)]
+    res = load_dataline(spec, dataline)
+    assert len(res) == 2
+    assert res[0].foo == 2
+    assert res[0].bar == 50
+    assert res[1].foo == 1
+    assert math.isnan(res[1].bar)
diff --git a/tests/test_variable_card.py b/tests/test_variable_card.py
index 145db051..726a8444 100644
--- a/tests/test_variable_card.py
+++ b/tests/test_variable_card.py
@@ -20,14 +20,18 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
+import dataclasses
 import math
 
-from ansys.dyna.core import Deck
 from ansys.dyna.core.lib.format_type import format_type
 from ansys.dyna.core.lib.variable_card import VariableCard
 
 import pytest
 
+@dataclasses.dataclass
+class bi:
+    foo: float = None
+    bar: float = None
 
 @pytest.mark.keywords
 def test_variable_card_length_function():
@@ -57,6 +61,15 @@ def test_variable_card_read(string_utils):
     assert v[1] == 2.0
     assert math.isnan(v[2])
 
+@pytest.mark.keywords
+def test_variable_card_read_struct(string_utils):
+    """Test a variable card using the struct type definition."""
+    v = VariableCard("bi", 8, 10, bi)
+    string = "     113.2    -50.01"
+    v.read(string_utils.as_buffer(string))
+    assert len(v) == 1
+    assert v[0].foo == 113.2
+    assert v[0].bar == -50.01
 
 @pytest.mark.keywords
 def test_variable_card_unbounded(string_utils):
@@ -94,17 +107,61 @@ def test_variable_card_set_single_value():
     rowdata = v._get_row_data(0, format_type.default)
     assert rowdata.startswith("      22.0"), f"incorrect rowdata: {rowdata}"
 
+@pytest.mark.keywords
+def test_variable_card_set_single_value_struct():
+    """test setting single value of bounded variable card"""
+    v = VariableCard("bi", 8, 10, bi, lambda: 3)
+    v[0] = bi(0.8, 0.9)
+    rowdata = v._get_row_data(0, format_type.default)
+    assert rowdata.startswith("       0.8       0.9"), f"incorrect rowdata: {rowdata}"
+    v[0] = (0.1, 1.2)
+    rowdata = v._get_row_data(0, format_type.default)
+    assert rowdata.startswith("       0.1       1.2"), f"incorrect rowdata: {rowdata}"
+    v[0] = [0.6, math.nan]
+    rowdata = v._get_row_data(0, format_type.default)
+    assert rowdata.startswith("       0.6          "), f"incorrect rowdata: {rowdata}"
 
 @pytest.mark.keywords
-def test_variable_car_write_long(ref_string):
+def test_variable_card_append_value_struct():
+    """test setting single value of bounded variable card"""
+    v = VariableCard("bi", 8, 10, bi)
+    v.append(bi(0.8, 0.9))
+    v.extend([
+        (0.1, 1.2),
+        [math.nan, 0.2]
+    ])
+    assert len(v) == 3
+    assert v[0] == bi(0.8,0.9)
+    assert v[1] == bi(0.1,1.2)
+    assert v[2] == bi(math.nan,0.2)
+
+@pytest.mark.keywords
+def test_variable_card_write_long(ref_string):
     """test writing unbounded long variable card with two rows."""
     v = VariableCard("bi", 8, 10, float)
-    v.format_type = format_type.long
     for i in range(10):
         v.append(i)
     assert v._num_rows() == 2
-    assert v.write(format_type.long) == ref_string.test_variable_card_string
+    assert v.write(format_type.long) == ref_string.test_variable_card_string_long
+    v.format = format_type.long
+    assert v.write() == ref_string.test_variable_card_string_long
+    assert v.write(format_type.default) == ref_string.test_variable_card_string
 
+@pytest.mark.keywords
+def test_variable_card_write_long_struct(ref_string):
+    """Test a variable card using the struct type definition."""
+    v = VariableCard("bi", 8, 10, bi)
+    for i in range(9):
+        val = i+1
+        v.append(bi(val, val/2))
+    assert v._num_rows() == 3
+
+    v_comment = "$#     foo       bar       foo       bar       foo       bar       foo       bar"
+    assert v._get_comment(format_type.default) == v_comment
+    standard_output = v.write()
+    assert standard_output == ref_string.test_variable_card_struct_string
+    long_output = v.write(format = format_type.long)
+    assert long_output == ref_string.test_variable_card_struct_string_long
 
 @pytest.mark.keywords
 def test_variable_card_read_long(string_utils):
@@ -118,3 +175,45 @@ def test_variable_card_read_long(string_utils):
     assert v[0] == 1.0
     assert v[1] == 2.0
     assert math.isnan(v[2])
+
+@pytest.mark.keywords
+def test_variable_card_read_long_struct(ref_string, string_utils):
+    """Test a variable card using the struct type definition."""
+    v = VariableCard("bi", 8, 10, bi)
+    v.format = format_type.long
+    string = "               113.2              -50.01                                        "
+    v.read(string_utils.as_buffer(string))
+    assert len(v) == 2
+    assert v[0].foo == 113.2
+    assert v[0].bar == -50.01
+    assert math.isnan(v[1].foo)
+    assert math.isnan(v[1].bar)
+
+    v = VariableCard("bi", 8, 10, bi)
+    v.read(string_utils.as_buffer(ref_string.test_variable_card_struct_string))
+    assert len(v) == 9
+
+    v = VariableCard("bi", 8, 10, bi)
+    v.format = format_type.long
+    v.read(string_utils.as_buffer(ref_string.test_variable_card_struct_string_long))
+    assert len(v) == 9
+
+@pytest.mark.keywords
+def test_variable_card_write_struct(string_utils):
+    """Test a variable card using the struct type definition."""
+
+    string = "       1.0       2.0       3.0"
+    v = VariableCard("bi", 8, 10, bi, lambda: 2)
+    v.read(string_utils.as_buffer(string))
+    assert len(v) == 2
+    assert v[0].foo == 1.0
+    assert v[0].bar == 2.0
+    assert v[1].foo == 3.0
+    assert math.isnan(v[1].bar)
+    assert v._num_rows() == 1
+    result = v.write(comment=False)
+    string = "       1.0       2.0       3.0          "
+    assert result == string
+    result = v.write(comment=True)
+    string = "$#     foo       bar       foo       bar\n       1.0       2.0       3.0          "
+    assert result == string
diff --git a/tests/testfiles/keywords/reference_string.py b/tests/testfiles/keywords/reference_string.py
index 15795d72..127c247f 100644
--- a/tests/testfiles/keywords/reference_string.py
+++ b/tests/testfiles/keywords/reference_string.py
@@ -150,6 +150,12 @@ nodeset
   70540871  70540875  70540887  70540888  70540890  70563790  70563792  70563794
   70573162"""
 
+test_set_node_list = """*SET_NODE_LIST
+$#     sid       da1       da2       da3       da4    solver       its         -
+        96                                        MECH      1                   
+$#    nid1      nid2      nid3      nid4      nid5      nid6      nid7      nid8
+      3224      3225      3226      3324      3325      3326      3327      3227		"""
+
 test_mat_piecewise_linear_plasticity_title = """*MAT_PIECEWISE_LINEAR_PLASTICITY_TITLE
 mat24
   690000022.24507E-6        1.       0.3       0.1        0.     1.E30          
@@ -1046,6 +1052,17 @@ $#     sid       da1       da2       da3       da4    solver
 $#   parts     parts     parts
          1         2         3"""
 
+test_set_part_list_generate_ref1 = """*SET_PART_LIST_GENERATE
+$#     sid       da1       da2       da3       da4    solver
+         1       0.0       0.0       0.0       0.0MECH      
+$#    bbeg      bend      bbeg      bend      bbeg      bend
+   2000000   2000025   2000100   2000200   2000500   2000600"""
+   
+test_set_part_list_generate_ref2 = """*SET_PART_LIST_GENERATE
+         3       0.0       0.0       0.0       0.0
+   2000000   2000025   2000100   2000200   2000500   2000600
+   2000700   2000800   2000900   2001000"""
+
 test_contact_1d_id_mpp1_mpp2 = """*CONTACT_1D_ID_MPP
 $#     cid                                                               heading
                                                                                 
@@ -1092,10 +1109,23 @@ $#             secid                area               thick
                                                             
 *END"""
 
-test_variable_card_string = """$#                bi                  bi                  bi                  bi                  bi                  bi                  bi                  bi
+test_variable_card_string = """$#      bi        bi        bi        bi        bi        bi        bi        bi
+       0.0       1.0       2.0       3.0       4.0       5.0       6.0       7.0
+       8.0       9.0"""
+test_variable_card_string_long = """$#                bi                  bi                  bi                  bi                  bi                  bi                  bi                  bi
                  0.0                 1.0                 2.0                 3.0                 4.0                 5.0                 6.0                 7.0
                  8.0                 9.0"""
 
+test_variable_card_struct_string = """$#     foo       bar       foo       bar       foo       bar       foo       bar
+       1.0       0.5       2.0       1.0       3.0       1.5       4.0       2.0
+       5.0       2.5       6.0       3.0       7.0       3.5       8.0       4.0
+       9.0       4.5"""
+
+test_variable_card_struct_string_long = """$#               foo                 bar                 foo                 bar                 foo                 bar                 foo                 bar
+                 1.0                 0.5                 2.0                 1.0                 3.0                 1.5                 4.0                 2.0
+                 5.0                 2.5                 6.0                 3.0                 7.0                 3.5                 8.0                 4.0
+                 9.0                 4.5"""
+
 test_deck_with_unknown_keywords = """*KEYWORD
 *NOT_REAL_KEYWORD
 $#    what        is      this