Repository: ansys/pydyna

Issue:
ansys.dyna.core.lib.deck.import_file does not read keywords properly when a card contains more than 80 characters
### 🔍 Before submitting the issue

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

### 🐞 Description of the bug

When importing an LS-DYNA inputdeck to a deck from a file, keywords that contain more than 80 characters per line are not recognized as keyword classes and end up in deck.string_keywords.

Trailing characters should be ignored. This is very common as people end up editing the files manually.

### 📝 Steps to reproduce

test_trailing_spaces.txt [test_trailing_spaces.txt](https://github.com/user-attachments/files/17775011/test_trailing_spaces.txt)
contains identical keywords with the exception of a few trailing spaces (here marked with [...):]
```
*KEYWORD
*CONTACT_TIED_SHELL_EDGE_TO_SURFACE_BEAM_OFFSET_ID
99999999  MYCONTACT ADDITIONAL SPACES LINE > 80 CHARS                                          
$#    soft    sofscl    lcidab    maxpar     sbopt     depth     bsort    frcfrq
  99999999  99999999         4         2                             0         0
$#    soft    sofscl    lcidab    maxpar     sbopt     depth     bsort    frcfrq
                                                40         0                    
$#    soft    sofscl    lcidab    maxpar     sbopt     depth     bsort    frcfrq
                           -5.       -5.                                      
$#    soft    sofscl    lcidab    maxpar     sbopt     depth     bsort    frcfrq
                 0.1               1.025         0         2                   0
$#  penmax    thkopt    shlthk     snlog      isym     i2d3d    sldthk    sldstf
                                                            
$#    igap    ignore    dprfac    dtstif     edgek    unused    flangl   cid_rcf
         1                 0.0       0.0                                        
$#   q2tri    dtpchk     sfnbr    fnlscl    dnlscl      tcso    tiedid    shledg
                                                                     1          ...
$#  sharec    cparm8    ipback     srnde    fricsf      icor     ftorq    region
                             1                 1.0     
*CONTACT_TIED_SHELL_EDGE_TO_SURFACE_BEAM_OFFSET_ID
99999999  MYCONTACT NO ADDITION SPACES                                        
$#    soft    sofscl    lcidab    maxpar     sbopt     depth     bsort    frcfrq
  99999999  99999999         4         2                             0         0
$#    soft    sofscl    lcidab    maxpar     sbopt     depth     bsort    frcfrq
                                                40         0                    
$#    soft    sofscl    lcidab    maxpar     sbopt     depth     bsort    frcfrq
                           -5.       -5.                                      
$#    soft    sofscl    lcidab    maxpar     sbopt     depth     bsort    frcfrq
                 0.1               1.025         0         2                   0
$#  penmax    thkopt    shlthk     snlog      isym     i2d3d    sldthk    sldstf
                                                            
$#    igap    ignore    dprfac    dtstif     edgek    unused    flangl   cid_rcf
         1                 0.0       0.0                                        
$#   q2tri    dtpchk     sfnbr    fnlscl    dnlscl      tcso    tiedid    shledg
                                                                     1          
$#  sharec    cparm8    ipback     srnde    fricsf      icor     ftorq    region
                             1                 1.0 
*END
```

```
from ansys.dyna.core.lib.deck import Deck
from ansys.dyna.core.keywords import keywords as kwd
test= Deck()
test.import_file('test_trailing_spaces.txt')
test.string_keywords
test.keywords
```

Both keywords should end up in test.keywords. Currently the one with trailing spaces ends up in test.string_keywords

### 💻 Which operating system are you using?

Windows

### 📀 Which ANSYS version are you using?

_No response_

### 🐍 Which Python version are you using?

3.12

### 📦 Installed packages

```shell
ansys-api-dyna==0.4.1
ansys-api-platform-instancemanagement==1.1.0
ansys-dpf-core==0.12.2
-e git+https://github.com/pyansys/pyDyna@11661b7119687e7d762847cff2d3d3643492dcf4#egg=ansys_dyna_core
ansys-platform-instancemanagement==1.1.2
ansys-tools-path==0.7.0
appdirs==1.4.4
asttokens==2.4.1
cachetools==5.5.0
certifi==2024.8.30
charset-normalizer==3.4.0
click==8.1.7
colorama==0.4.6
comm==0.2.2
contourpy==1.3.1
cycler==0.12.1
debugpy==1.8.8
decorator==5.1.1
executing==2.1.0
fonttools==4.54.1
google-api-core==2.23.0
google-api-python-client==2.152.0
google-auth==2.36.0
google-auth-httplib2==0.2.0
googleapis-common-protos==1.66.0
grpcio==1.67.1
hollerith==0.4.1
httplib2==0.22.0
idna==3.10
importlib_metadata==8.5.0
ipykernel==6.29.5
ipython==8.29.0
jedi==0.19.2
jupyter_client==8.6.3
jupyter_core==5.7.2
kiwisolver==1.4.7
matplotlib==3.9.2
matplotlib-inline==0.1.7
nest-asyncio==1.6.0
numpy==2.1.3
packaging==24.2
pandas==2.2.3
parso==0.8.4
pillow==11.0.0
platformdirs==4.3.6
pooch==1.8.2
prompt_toolkit==3.0.48
proto-plus==1.25.0
protobuf==5.28.3
psutil==6.1.0
pure_eval==0.2.3
pyasn1==0.6.1
pyasn1_modules==0.4.1
Pygments==2.18.0
pyparsing==3.2.0
python-dateutil==2.9.0.post0
pytz==2024.2
pyvista==0.44.1
pywin32==308
pyzmq==26.2.0
requests==2.32.3
rsa==4.9
scooby==0.10.0
setuptools==75.5.0
six==1.16.0
stack-data==0.6.3
tornado==6.4.1
tqdm==4.67.0
traitlets==5.14.3
typing_extensions==4.12.2
tzdata==2024.2
uritemplate==4.1.1
urllib3==2.2.3
vtk==9.3.1
wcwidth==0.2.13
zipp==3.21.0
```

Hints from issue comments:
@koubaa: @RaphaelHeiniger I don't see anything in the manual that says that characters after the limit are ignored. Is this something that is consistently done for all keywords or are there special cases? Do you want pydyna to always ignore characters after the limit? Or would the user have to opt-into a mode where those characters are ignored? Should a warning be issued?@RaphaelHeiniger: @koubaa it was just somewhere in my head so I could not answer straight away. But I found the section in the manual in the Getting Started chapter:


**GENERAL CARD FORMAT** 
The following sections specify, for each keyword command, the cards that must be defined and those cards that are optional. Each card is described in its fixed format form and is shown as a number of fields in an 80 character string. With the exception of “long format input” as described later in this section, most input cards consist of 8 fields with a  field length of 10 characters. A sample card is shown below. The card format is clearly  stated if the format is different than 8 fields of 10 characters. See also notes on a special “I10 format” at the end of this section. As an alternative to fixed format, a card may be in free format with the values of the variables separated by commas. When using comma-delimited values on a card, the number of characters used to specify a value must not exceed the field length for fixed format. For example, an I8 number is limited to a value of 99999999 and a larger number with more than 8 characters is unacceptable. **A further restriction is that characters beyond column 80 of each line are ignored by the code.** Fixed format and free, comma delimited format can be mixed throughout the deck and even within different cards of a single command but not within a card.

So one has to handle the long format correctly but otherwise I would simply ignore the extra characters by default and output a warning e.g. "Detected outbound card length while reading. Ignoring outbound characters "bla bla"."


Solution patch:
diff --git a/doc/changelog/647.miscellaneous.md b/doc/changelog/647.miscellaneous.md
new file mode 100644
index 00000000..156fd60b
--- /dev/null
+++ b/doc/changelog/647.miscellaneous.md
@@ -0,0 +1 @@
+fix: warn out of bounds characters when reading lines
\ No newline at end of file
diff --git a/src/ansys/dyna/core/lib/kwd_line_formatter.py b/src/ansys/dyna/core/lib/kwd_line_formatter.py
index ee28b854..e780900d 100644
--- a/src/ansys/dyna/core/lib/kwd_line_formatter.py
+++ b/src/ansys/dyna/core/lib/kwd_line_formatter.py
@@ -21,6 +21,7 @@
 # SOFTWARE.
 
 import typing
+import warnings
 
 from ansys.dyna.core.lib.parameter_set import ParameterSet
 
@@ -146,6 +147,7 @@ def load_dataline(spec: typing.List[tuple], line_data: str, parameter_set: Param
         data.append(value)
 
     if end_position < len(line_data):
-        raise Exception("Data line is too long!")
+        warning_message = f'Detected out of bound card characters:\n"{line_data[end_position:]}"\n"Ignoring.'
+        warnings.warn(warning_message)
 
     return tuple(data)


Test patch:
diff --git a/tests/test_card.py b/tests/test_card.py
index fdd6ae56..ec2162c0 100644
--- a/tests/test_card.py
+++ b/tests/test_card.py
@@ -40,7 +40,7 @@ def test_load_card_errors(string_utils):
         # cards can only load a readable buffer
         card.read("")
 
-    with pytest.raises(Exception):
+    with pytest.warns(UserWarning, match="Detected out of bound card characters"):
         # error if the line that is too long
         buf = "                                           "
         card.read(string_utils.as_buffer(buf))
diff --git a/tests/test_keywords.py b/tests/test_keywords.py
index 1e4468eb..b7fb7e92 100644
--- a/tests/test_keywords.py
+++ b/tests/test_keywords.py
@@ -162,6 +162,11 @@ $#   secid      area     thick
     # ensure that it does not throw when the data line is empty
     belt.loads(seatbelt_text)
 
+    seatbelt_text = seatbelt_text + "..."
+    # ensure that it does not throw when extra characters are added
+    with pytest.warns(UserWarning, match="Detected out of bound card characters"):
+        belt.loads(seatbelt_text)
+
 
 @pytest.mark.keywords
 def test_read_nodes(ref_string):
diff --git a/tests/test_load_dataline.py b/tests/test_load_dataline.py
index f9f3944e..bc5c0f62 100644
--- a/tests/test_load_dataline.py
+++ b/tests/test_load_dataline.py
@@ -65,7 +65,7 @@ def test_load_dataline_005():
 @pytest.mark.keywords
 def test_load_dataline_006():
     """test loading a data line that is too long."""
-    with pytest.raises(Exception):
+    with pytest.warns(UserWarning, match="Detected out of bound card characters"):
         load_dataline([(0, 8, int), (8, 8, float), (16, 8, float)], "                                          ")