Repository: scipy/scipy

Issue:
BUG: savemat(..., format="4") throws ValueError errorneously for char arrays
### Describe your issue.

When trying to use `savemat("deleteme.mat", dct, format="4")` 

the function `arr_to_2d()` inside `scipy/io/matlab/_mio4.py` throws an exception that the supplied array must be 1- or 2-D despite being so.

## Possible reason for the bug
A possible reason for the error is, that in function `write_char()` <https://github.com/scipy/scipy/blob/bf188fbe0a8e30f36381c91d0e6acd4241bcae7f/scipy/io/matlab/_mio4.py#L548> the function `arr_to_chars(arr)` is called which tries to convert the array to an char array although it already is one. During the conversion, this function adds a dimension resulting in a `len(shape) > 2` which triggers the ValueError in the subsequent function call of `arr_to_2d`


## Possible fix:
One could check, if the dtype of the supplied array is already in a char array format and skip the conversion call to `array_to_chars()`. This should happend before <https://github.com/scipy/scipy/blob/bf188fbe0a8e30f36381c91d0e6acd4241bcae7f/scipy/io/matlab/_mio4.py#L549>.

### Reproducing Code Example

```python
import numpy as np
from scipy.io import savemat

dct = {
    "char_array": np.array([list("char"), list("char")], dtype="U1"),
    "string_array": np.array(["string", "string"]),
}
savemat("deleteme.mat", dct, format="4")
```


### Error message

```shell
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[1], line 8
      2 from scipy.io import savemat
      4 dct = {
      5     "char_array": np.array([list("char"), list("char")], dtype="U1"),
      6     "string_array": np.array(["string", "string"]),
      7 }
----> 8 savemat("deleteme.mat", dct, format="4")

File ~/temp/scipy/scipy/io/matlab/_mio.py:301, in savemat(file_name, mdict, appendmat, format, long_field_names, do_compression, oned_as)
    299 else:
    300     raise ValueError("Format should be '4' or '5'")
--> 301 MW.put_variables(mdict)

File ~/temp/scipy/scipy/io/matlab/_mio4.py:623, in MatFile4Writer.put_variables(self, mdict, write_header)
    621 self._matrix_writer = VarWriter4(self)
    622 for name, var in mdict.items():
--> 623     self._matrix_writer.write(var, name)

File ~/temp/scipy/scipy/io/matlab/_mio4.py:522, in VarWriter4.write(self, arr, name)
    520     raise TypeError('Cannot save void type arrays')
    521 elif dtt in (np.str_, np.bytes_):
--> 522     self.write_char(arr, name)
    523     return
    524 self.write_numeric(arr, name)

File ~/temp/scipy/scipy/io/matlab/_mio4.py:550, in VarWriter4.write_char(self, arr, name)
    548 def write_char(self, arr, name):
    549     arr = arr_to_chars(arr)
--> 550     arr = arr_to_2d(arr, self.oned_as)
    551     dims = arr.shape
    552     self.write_header(
    553         name,
    554         dims,
    555         P=miUINT8,
    556         T=mxCHAR_CLASS)

File ~/temp/scipy/scipy/io/matlab/_mio4.py:448, in arr_to_2d(arr, oned_as)
    446 dims = matdims(arr, oned_as)
    447 if len(dims) > 2:
--> 448     raise ValueError('Matlab 4 files cannot save arrays with more than '
    449                      '2 dimensions')
    450 return arr.reshape(dims)

ValueError: Matlab 4 files cannot save arrays with more than 2 dimensions
```


### SciPy/NumPy/Python version and system information

```shell
1.12.0.dev0+0.bf188fb 1.26.2 sys.version_info(major=3, minor=11, micro=3, releaselevel='final', serial=0)
Build Dependencies:
  blas:
    detection method: pkgconfig
    found: true
    include directory: /usr/include/x86_64-linux-gnu/openblas-pthread/
    lib directory: /usr/lib/x86_64-linux-gnu/openblas-pthread/
    name: openblas
    openblas configuration: USE_64BITINT= DYNAMIC_ARCH=1 DYNAMIC_OLDER=1 NO_CBLAS=
      NO_LAPACK= NO_LAPACKE=1 NO_AFFINITY=1 USE_OPENMP=0 generic MAX_THREADS=64
    pc file directory: /usr/lib/x86_64-linux-gnu/pkgconfig
    version: 0.3.13
  lapack:
    detection method: pkgconfig
    found: true
    include directory: /usr/include/x86_64-linux-gnu/openblas-pthread/
    lib directory: /usr/lib/x86_64-linux-gnu/openblas-pthread/
    name: openblas
    openblas configuration: USE_64BITINT= DYNAMIC_ARCH=1 DYNAMIC_OLDER=1 NO_CBLAS=
      NO_LAPACK= NO_LAPACKE=1 NO_AFFINITY=1 USE_OPENMP=0 generic MAX_THREADS=64
    pc file directory: /usr/lib/x86_64-linux-gnu/pkgconfig
    version: 0.3.13
  pybind11:
    detection method: config-tool
    include directory: unknown
    name: pybind11
    version: 2.11.1
Compilers:
  c:
    commands: gcc
    linker: ld.bfd
    name: gcc
    version: 10.2.1
  c++:
    commands: g++
    linker: ld.bfd
    name: gcc
    version: 10.2.1
  cython:
    commands: cython
    linker: cython
    name: cython
    version: 3.0.6
  fortran:
    commands: gfortran
    linker: ld.bfd
    name: gcc
    version: 10.2.1
  pythran:
    include directory: ../env/lib/python3.11/site-packages/pythran
    version: 0.14.0
Machine Information:
  build:
    cpu: x86_64
    endian: little
    family: x86_64
    system: linux
  cross-compiled: true
  host:
    cpu: x86_64
    endian: little
    family: x86_64
    system: linux
Python Information:
  path: ~/temp/scipy/env/bin/python
  version: '3.11'
```



Discussion:
Could you provide a minimal reproducing example please
I don't quite understand your request. In the code given above, I had already tried to give a minimal set of code lines. But I guess one could go with this MWE:

```python
import numpy, scipy; scipy.io.savemat("a", {"a": numpy.array([list("a")], dtype="U1"),},format="4")
```
Apologies, long day completely missed your original code snippet
No worries. I had also added a possible place where it can be fixed via a simple if statement. Unfortunately, I don't know the code base to have come up with a patch/PR.
Can I work on this issue? 
@SarthakNikhal we don't allocate issues but you are welcome to work on this

Resolving patch:
diff --git a/scipy/io/matlab/_mio4.py b/scipy/io/matlab/_mio4.py
--- a/scipy/io/matlab/_mio4.py
+++ b/scipy/io/matlab/_mio4.py
@@ -546,7 +546,8 @@ def write_numeric(self, arr, name):
             self.write_bytes(arr)
 
     def write_char(self, arr, name):
-        arr = arr_to_chars(arr)
+        if arr.dtype.type == np.str_ and arr.dtype.itemsize != np.dtype('U1').itemsize:
+            arr = arr_to_chars(arr)
         arr = arr_to_2d(arr, self.oned_as)
         dims = arr.shape
         self.write_header(


Test patch:
diff --git a/scipy/io/matlab/tests/test_mio.py b/scipy/io/matlab/tests/test_mio.py
--- a/scipy/io/matlab/tests/test_mio.py
+++ b/scipy/io/matlab/tests/test_mio.py
@@ -1327,3 +1327,13 @@ def test_gh_17992(tmp_path):
             new_dict)
     assert_allclose(new_dict["data"][0][0], array_one)
     assert_allclose(new_dict["data"][0][1], array_two)
+
+
+def test_gh_19659(tmp_path):
+    d = {
+        "char_array": np.array([list("char"), list("char")], dtype="U1"),
+        "string_array": np.array(["string", "string"]),
+        }
+    outfile = tmp_path / "tmp.mat"
+    # should not error:
+    savemat(outfile, d, format="4")