Source code for pyyeti.nastran.op4

# -*- coding: utf-8 -*-
"""
Python tools for reading/writing Nastran .op4 files. Can read and
write all formats (as far as I know) with the restrictions that the
output files created by this class are always double precision, and
all matrices are read in as double precision. The binary files can be
in big or little endian format.

Notes on sparse matrices:

  1. By default, matrices read from .op4 files will be regular
     :class:`numpy.ndarray` matrices. However, :mod:`scipy.sparse`
     matrices can be created instead. See the `sparse` option in
     :func:`read` and :func:`load`.

  2. By default, matrices written to .op4 files will follow the Python
     type: :class:`numpy.ndarray` matrices will be written in dense
     format and :mod:`scipy.sparse` matrices will be written in
     "bigmat" sparse format. This can be overridden by specifying the
     `sparse` option in :func:`write`.

.. note::
    Some features of this module are demonstrated in the pyYeti
    :ref:`tutorial`: :doc:`/tutorials/op4`. There is also a link to
    the source Jupyter notebook at the top of the tutorial.
"""

import itertools as it
import struct
import warnings
import collections
from collections.abc import Mapping
import numpy as np
import scipy.sparse as sp
from pyyeti import guitools


def _ensure_dp(m):
    """
    Ensure double precision values
    """
    if np.iscomplexobj(m):
        if m.dtype != np.complex128:
            return m.astype(np.complex128)
    elif m.dtype != np.float64:
        return m.astype(np.float64)
    return m


# ensure double precision 2d arrays or tuple as returned by
# scipy.sparse.find:
def _ensure_2d_dp(m):
    """
    Ensures 2d double precision array or tuple as returned by
    scipy.sparse.find
    """
    if sp.issparse(m):
        # return m
        i, j, v = sp.find(m)
        return m, i, j, _ensure_dp(v)
    m = np.atleast_2d(m)
    if m.ndim > 2:
        raise ValueError("found array with greater than 2 dimensions.")
    return _ensure_dp(m)


def _check_write_names(names):
    """
    Ensure valid names
    """
    outnames = []
    for i, name in enumerate(names):
        if not name.isidentifier():
            oldname, name = name, f"m{i}"
            warnings.warn(
                f"Matrix for output4 write has name: {oldname!r}. "
                f"Changing to {name!r}.",
                RuntimeWarning,
            )
        elif len(name) > 8:
            oldname, name = name, name[:8]
            warnings.warn(
                f"Matrix for output4 write has name: {oldname!r}. "
                f"Truncating to {name!r}.",
                RuntimeWarning,
            )
        outnames.append(name)
    return outnames


[docs] class OP4: """ Class for reading/writing Nastran output4 (.op4) files. See demo below and refer to the help on these functions for more information: :func:`write` (or :func:`save`), :func:`load` (or the lower level :func:`dctload`, :func:`listload`), and :func:`dir`. `save` is an alias for `write`. Examples -------- Instantiate the class and create matrices for demo: >>> from pyyeti.nastran import op4 >>> o4 = op4.OP4() >>> import numpy as np >>> rng = np.random.default_rng() >>> r = rng.normal(size=(3, 5)) >>> c = r + 1j*rng.normal(size=(3, 5)) Write binary op4 file, with 'r' first: >>> o4.write('testbin.op4', ['r', 'c'], [r, c]) Write ascii op4 file without caring about order: >>> o4.write('testascii.op4', dict(r=r, c=c), binary=False) To read an op4 file into a dictionary (indexed by the name in lower case): >>> dct = o4.load('testbin.op4', into='dct') Note: to preserve order, the dictionary returned by "load" or "read" is actually an OrderedDict from the standard Python "collections" module (:class:`collections.OrderedDict`). To read into a list: >>> names, mats, forms, mtypes = o4.load('testascii.op4', ... into='list') Check some results: >>> print(np.all(r == dct['r'][0])) True >>> if names[0] == 'c': ... print(np.all(c == mats[0])) ... else: ... print(np.all(c == mats[1])) True To print a 'directory' of an op4 file: >>> d = o4.dir('testbin.op4') r , 3 x 5 , form=2, mtype=2 c , 3 x 5 , form=2, mtype=4 Clean up: >>> import os >>> os.remove('testbin.op4') >>> os.remove('testascii.op4') """
[docs] def __init__(self): self._fileh = None string = "%.1E" % 1.2 self._expdigits = len(string) - (string.find("E") + 2) self._rows4bigmat = 65536 # Tunable value ... if number of values exceeds this, read # with numpy.fromfile instead of struct.unpack. self._rowsCutoff = 3000 self.save = self.write
def __del__(self): if self._fileh: self._fileh.close() self._fileh = None def _op4close(self): if self._fileh: self._fileh.close() self._fileh = None def _decode_format(self, bytes_): if min(bytes_[:4]) == 0: self._ascii = False # determine if big-endian or little-endian: reclen = np.frombuffer(bytes_[:4], "<u4") if reclen <= 48: self._endian = "<" else: self._endian = ">" reclen = np.frombuffer(bytes_[:4], ">u4") # determine if 64 bit ints or 32 bit ints: if reclen == 24: self._bit64 = False else: self._bit64 = True else: self._ascii = True def _op4open_read(self, filename): """ Open binary or ascii op4 file for reading. Sets these class variables: _fileh : file handle Value returned by open(). File is opened in 'r' mode if ascii, 'rb' mode if binary. _ascii : bool True if file is ascii. _dformat : bool True if an ascii file uses 'D' instead of 'E' (eg, 1.4D3 instead of 1.4E3) _bit64 : True or False True if 'key' integers are 64-bit in binary op4 files. _endian : string Either '>' or '<' for big-endian and little-endian, respectively. Only used for binary files. _Str_i4 : struct.Struct object Precompiled for reading 4 byte integers _Str_i : struct.Struct object Precompiled for reading 4 or 8 byte integers _bytes_i : integer Either 4 or 8, to go with Str_i. _str_sr : string Either self._endian + '%df' or self._endian + '%dd', depending on self._bit64; for reading single precision reals. _bytes_sr : integer Number of bytes in single real. _str_dr : string self._endian + '%dd', for reading double precision reals. _wordsperdouble : integer Either 1 or 2; 2 if self._bit64 is False. """ self._fileh = open(filename, "rb") bytes_ = self._fileh.read(16) if len(bytes_) < 16: self._op4close() raise RuntimeError( f'"{filename}" is empty or nearly empty (has {len(bytes_)} bytes)' ) self._decode_format(bytes_) self._dformat = False self._matcount = 0 if self._ascii: self._fileh.readline() self._fileh.readline() line = self._fileh.readline().decode() # sparse formats have integer header line: if line.find(".") == -1: line = self._fileh.readline().decode() if line.find("D") > -1: self._dformat = True self._fileh.close() self._fileh = open(filename, "r") else: self._Str_i4 = struct.Struct(self._endian + "i") if self._bit64: self._Str_i = struct.Struct(self._endian + "q") self._bytes_i = 8 self._Str_ii = struct.Struct(self._endian + "qq") self._bytes_ii = 16 self._Str_iii = struct.Struct(self._endian + "3q") self._bytes_iii = 24 self._Str_iiii = struct.Struct(self._endian + "4q") self._bytes_iiii = 32 self._str_sr = self._endian + "%dd" self._str_sr_fromfile = np.dtype(self._endian + "f8") self._bytes_sr = 8 self._wordsperdouble = 1 else: self._Str_i = self._Str_i4 self._bytes_i = 4 self._Str_ii = struct.Struct(self._endian + "ii") self._bytes_ii = 8 self._Str_iii = struct.Struct(self._endian + "3i") self._bytes_iii = 12 self._Str_iiii = struct.Struct(self._endian + "4i") self._bytes_iiii = 16 self._str_sr = self._endian + "%df" self._str_sr_fromfile = np.dtype(self._endian + "f4") self._bytes_sr = 4 self._wordsperdouble = 2 self._str_dr = self._endian + "%dd" self._str_dr_fromfile = np.dtype(self._endian + "f8") self._fileh.seek(0) def _skipop4_ascii(self, perline, rows, cols, mtype): """ Skip an op4 matrix - ascii. Parameters ---------- perline : integer Number of elements per line in the file. rows : integer Number of rows in matrix. cols : integer Number of columns in matrix. mtype : integer Nastran matrix type. Returns ------- None On entry, file is positioned after the title line, but before the first column is printed. On exit, the file is positioned so the next readline will get the next title line. """ # read until next matrix: bigmat = rows < 0 or rows >= self._rows4bigmat if mtype & 1: wper = 1 else: wper = 2 line = self._fileh.readline() c_slice = slice(0, 8) r_slice = slice(8, 16) e_slice = slice(16, 24) c = int(line[c_slice]) - 1 r = int(line[r_slice]) if r > 0: while c < cols: elems = int(line[e_slice]) nlines = (elems + perline - 1) // perline for _ in it.repeat(None, nlines): self._fileh.readline() line = self._fileh.readline() c = int(line[c_slice]) - 1 elif bigmat: while c < cols: elems = int(line[e_slice]) while elems > 0: line = self._fileh.readline() L = int(line[c_slice]) - 1 # L elems -= L + 2 L //= wper # read column as a long string nlines = (L + perline - 1) // perline for _ in it.repeat(None, nlines): self._fileh.readline() line = self._fileh.readline() c = int(line[c_slice]) - 1 else: while c < cols: elems = int(line[e_slice]) while elems > 0: line = self._fileh.readline() IS = int(line) L = (IS >> 16) - 1 # L elems -= L + 1 L //= wper # read column as a long string nlines = (L + perline - 1) // perline for _ in it.repeat(None, nlines): self._fileh.readline() line = self._fileh.readline() c = int(line[c_slice]) - 1 self._fileh.readline() def _check_name(self, name): """ Check name read from op4 file: strip all blanks/nulls; if name is not a valid Python identifier, it is set to f"m{self._matcount}". self._matcount starts at 0 and is incremented in this routine. Returns new name (usually the same as the input name). """ name = name.strip(" \x00").replace(" ", "").replace("\x00", "").lower() if not name.isidentifier(): oldname, name = name, f"m{self._matcount}" warnings.warn( f"Output4 file has matrix name: {oldname!r}. Changing to {name!r}.", RuntimeWarning, ) self._matcount += 1 return name def _get_ascii_block(self, L, perline, linelen): fh = self._fileh nlines = (L - 1) // perline + 1 blocklist = [ln[:linelen] for ln in it.islice(fh, nlines)] s = "".join(blocklist) if self._dformat: s = s.replace("D", "E") return s @staticmethod def _init_dense_real(rows, cols): return np.zeros((rows, cols), dtype=float, order="F") @staticmethod def _init_dense_complex(rows, cols): return np.zeros((rows, cols), dtype=complex, order="F") @staticmethod def _put_ascii_values(X, r, c, s, L, numlen): a = 0 for i in range(L): b = a + numlen X[r + i, c] = s[a:b] a = b @staticmethod def _put_ascii_values_c(X, r, c, s, L, numlen): a = 0 for i in range(L // 2): b = a + numlen real = float(s[a:b]) a = b b = a + numlen imag = float(s[a:b]) a = b X[r + i, c] = real + 1j * imag @staticmethod def _dense_matrix(rows, cols, X): return X @staticmethod def _init_sparse(rows, cols): return [], [], [] @staticmethod def _put_ascii_values_sparse(X, r, c, s, L, numlen): I, J, V = X a = 0 for i in range(L): b = a + numlen I.append(r + i) J.append(c) V.append(float(s[a:b])) a = b @staticmethod def _put_ascii_values_sparse_c(X, r, c, s, L, numlen): I, J, V = X a = 0 for i in range(L // 2): b = a + numlen real = float(s[a:b]) a = b b = a + numlen imag = float(s[a:b]) a = b I.append(r + i) J.append(c) V.append(real + 1j * imag) @staticmethod def _sparse_matrix(rows, cols, X): I, J, V = X return sp.coo_matrix((np.array(V), (I, J)), shape=(rows, cols)) def _rd_dense_ascii( self, wper, r, c, rows, cols, line, numlen, perline, linelen, funcs ): init, put, retrn = funcs X = init(rows, cols) c_slice = slice(0, 8) r_slice = slice(8, 16) e_slice = slice(16, 24) while c < cols: elems = int(line[e_slice]) r -= 1 # read column as a long string s = self._get_ascii_block(elems, perline, linelen) put(X, r, c, s, elems, numlen) line = self._fileh.readline() c = int(line[c_slice]) - 1 r = int(line[r_slice]) return retrn(rows, cols, X) def _rd_bigmat_ascii( self, wper, r, c, rows, cols, line, numlen, perline, linelen, funcs ): init, put, retrn = funcs X = init(rows, cols) c_slice = slice(0, 8) r_slice = slice(8, 16) e_slice = slice(16, 24) while c < cols: elems = int(line[e_slice]) while elems > 0: line = self._fileh.readline() L = int(line[c_slice]) - 1 # L r = int(line[r_slice]) - 1 # irow-1 elems -= L + 2 L //= wper s = self._get_ascii_block(L, perline, linelen) put(X, r, c, s, L, numlen) line = self._fileh.readline() c = int(line[c_slice]) - 1 return retrn(rows, cols, X) def _rd_nonbigmat_ascii( self, wper, r, c, rows, cols, line, numlen, perline, linelen, funcs ): init, put, retrn = funcs X = init(rows, cols) c_slice = slice(0, 8) # r_slice = slice(8, 16) e_slice = slice(16, 24) while c < cols: elems = int(line[e_slice]) while elems > 0: line = self._fileh.readline() IS = int(line) L = (IS >> 16) - 1 # L r = IS - ((L + 1) << 16) - 1 # irow-1 elems -= L + 1 L //= wper s = self._get_ascii_block(L, perline, linelen) put(X, r, c, s, L, numlen) line = self._fileh.readline() c = int(line[c_slice]) - 1 return retrn(rows, cols, X) def _get_funcs(self, a_or_b, rows, r, mtype, sparse, allzeros): if a_or_b == "ascii": rd_dense = self._rd_dense_ascii rd_bigmat = self._rd_bigmat_ascii rd_nonbigmat = self._rd_nonbigmat_ascii put_values = OP4._put_ascii_values put_values_c = OP4._put_ascii_values_c put_values_sparse = OP4._put_ascii_values_sparse put_values_sparse_c = OP4._put_ascii_values_sparse_c else: rd_dense = self._rd_dense_binary rd_bigmat = self._rd_bigmat_binary rd_nonbigmat = self._rd_nonbigmat_binary put_values = OP4._put_binary_values put_values_c = OP4._put_binary_values_c put_values_sparse = OP4._put_binary_values_sparse put_values_sparse_c = OP4._put_binary_values_sparse_c if r > 0: if allzeros and rows < 0: # assume bigmat sparse format # for example: # 19 -10 2 4C2 1P,3E22.15 # 20 1 2 # 1.000000000000000E+00 rdfunc = rd_bigmat if sparse is None: sparse = True else: # dense format rdfunc = rd_dense if sparse is None: sparse = False else: # either bigmat or nonbigmat sparse format: if sparse is None: sparse = True if rows < 0 or rows >= self._rows4bigmat: # bigmat sparse format: # 6 -5 2 4C1 1P,3E22.15 # 1 0 22 # 21 1 # 1.233140833282189E+00-1.364201536870681E+00 ... rdfunc = rd_bigmat else: # nonbigmat sparse format # 6 5 2 4C1 1P,3E22.15 # 1 0 21 # 1376257 # 1.233140833282189E+00-1.364201536870681E+00 ... rdfunc = rd_nonbigmat if not sparse: if mtype < 3: funcs = (OP4._init_dense_real, put_values, OP4._dense_matrix) else: funcs = (OP4._init_dense_complex, put_values_c, OP4._dense_matrix) else: if mtype < 3: put = put_values_sparse else: put = put_values_sparse_c funcs = (OP4._init_sparse, put, OP4._sparse_matrix) return rdfunc, funcs @staticmethod def _get_sparsefunc(sparse): try: sparse, sparsefunc = sparse except TypeError: sparsefunc = None return sparse, sparsefunc def _loadop4_ascii(self, patternlist=None, listonly=False, sparse=False): """ Reads next matching matrix or returns information on the next matrix in the ascii op4 file. Parameters ---------- patternlist : list List of string patterns; each matrix name is matched against this list: if it matches any of the patterns, it is read in. listonly : bool True if only reading name. sparse : bool or None or two-tuple_like; optional Specifies whether output matrices will be regular numpy arrays or sparse arrays. If not tuple_like: ======== =============================================== `sparse` Action ======== =============================================== None Auto setting: each matrix will be sparse if and only if it was written in a sparse format True Matrices will be returned in sparse format False Matrices will be returned in regular (dense) numpy arrays ======== =============================================== If `sparse` is two-tuple_like, the first element is either None, True, or False (see table above) and the second element is a callable, as in: ``X = callable(X)``. A common usage of the callable would be to convert from "COO" sparse form (see :class:`scipy.sparse.coo_matrix`) to a more desirable form. For example, to ensure *all* matrices are returned in CSC form (see :class:`scipy.sparse.csc_matrix`) use:: sparse=(True, scipy.sparse.coo_matrix.tocsc) The callable is ignored for non-sparse matrices. Returns ------- tuple: (name, matrix, form, mtype) name : string Lower-case name of matrix. matrix : 2d ndarray The matrix. form : integer Nastran form of matrix. mtype : integer Nastran matrix type. Notes ----- - All outputs will be None if reached EOF. - The `matrix` output will be [rows, cols] of the matrix if the matrix is skipped. - The default form for sparse matrices is the "COO" sparse form (see :class:`scipy.sparse.coo_matrix`). To override, provide a callable in the `sparse` option (see above). """ while 1: line = self._fileh.readline() line = line.rstrip() if line == "": return None, None, None, None if line.endswith("|I16"): line = line[:-4] c_slice = slice(0, 16) r_slice = slice(16, 32) f_slice = slice(32, 40) t_slice = slice(40, 48) n_slice = slice(48, 56) else: c_slice = slice(0, 8) r_slice = slice(8, 16) f_slice = slice(16, 24) t_slice = slice(24, 32) n_slice = slice(32, 40) cols = int(line[c_slice]) rows = int(line[r_slice]) form = int(line[f_slice]) mtype = int(line[t_slice]) length = len(line) name = self._check_name(line[n_slice]) perline = 5 numlen = 16 if length > n_slice.stop: # 1P,3E24.16 <-- starts at position n_slice.stop # 3e24.16 numformat = line[n_slice.stop :].strip().upper() if numformat.startswith("1P,"): numformat = numformat[3:] p = numformat.replace("D", "E").find("E") if p > 0: perline = int(numformat[:p]) numlen = int(numformat[p + 1 :].split(".")[0]) if patternlist and name not in patternlist: skip = 1 else: skip = 0 if listonly or skip: self._skipop4_ascii(perline, rows, cols, mtype) if listonly: return name, (abs(rows), cols), form, mtype else: break wper = 1 if mtype & 1 else 2 line = self._fileh.readline() linelen = perline * numlen c_slice = slice(0, 8) r_slice = slice(8, 16) c = int(line[c_slice]) - 1 r = int(line[r_slice]) sparse, sparsefunc = OP4._get_sparsefunc(sparse) rdfunc, funcs = self._get_funcs("ascii", rows, r, mtype, sparse, c >= cols) X = rdfunc(wper, r, c, abs(rows), cols, line, numlen, perline, linelen, funcs) if sparsefunc and sp.issparse(X): X = sparsefunc(X) self._fileh.readline() return name, X, form, mtype def _skipop4_binary(self, cols): """ Skip a binary op4 matrix. Parameters ---------- cols : integer Number of columns in matrix. """ # Scan matrix by column icol = 1 bi = self._bytes_i delta = 4 - bi while icol <= cols: # Read record length at start of record: reclen = self._Str_i4.unpack(self._fileh.read(4))[0] # Read column header icol = self._Str_i.unpack(self._fileh.read(bi))[0] self._fileh.seek(reclen + delta, 1) def _get_cutoff_etc(self): return ( self._rowsCutoff, self._Str_iii.unpack, self._bytes_iii, self._Str_i4.unpack, ) def _get_s2(self): return self._Str_ii.unpack, self._bytes_ii def _get_s1(self): return self._Str_i.unpack, self._bytes_i @staticmethod def _put_binary_values(X, r, c, Y): X[r : r + len(Y), c] = Y @staticmethod def _put_binary_values_c(X, r, c, Y): if not isinstance(Y, np.ndarray): Y = np.array(Y) Y = Y.astype("float", copy=False) Y.dtype = complex X[r : r + len(Y), c] = Y @staticmethod def _put_binary_values_sparse(X, r, c, Y): I, J, V = X for i, v in enumerate(Y): I.append(r + i) J.append(c) V.append(v) @staticmethod def _put_binary_values_sparse_c(X, r, c, Y): I, J, V = X for i, j in enumerate(range(0, len(Y), 2)): I.append(r + i) J.append(c) V.append(Y[j] + 1j * Y[j + 1]) def _rd_dense_binary( self, fp, wper, r, c, rows, cols, nwords, reclen, bytesreal, numform, numform2, funcs, ): init, put, retrn = funcs X = init(rows, cols) cutoff, s3, b3, s4 = self._get_cutoff_etc() while c < cols: r -= 1 nwords //= wper if nwords < cutoff: Y = struct.unpack(numform % nwords, fp.read(bytesreal * nwords)) else: Y = np.fromfile(fp, numform2, nwords) put(X, r, c, Y) fp.read(4) reclen = s4(fp.read(4))[0] c, r, nwords = s3(fp.read(b3)) c -= 1 return retrn(rows, cols, X), reclen def _rd_bigmat_binary( self, fp, wper, r, c, rows, cols, nwords, reclen, bytesreal, numform, numform2, funcs, ): init, put, retrn = funcs X = init(rows, cols) cutoff, s3, b3, s4 = self._get_cutoff_etc() s2, b2 = self._get_s2() while c < cols: # bigmat sparse format # Read column data, one string of numbers at a time # (strings of zeros are skipped) while nwords > 0: L, r = s2(fp.read(b2)) nwords -= L + 1 L = (L - 1) // wper r -= 1 if L < cutoff: Y = struct.unpack(numform % L, fp.read(bytesreal * L)) else: Y = np.fromfile(fp, numform2, L) put(X, r, c, Y) fp.read(4) reclen = s4(fp.read(4))[0] c, r, nwords = s3(fp.read(b3)) c -= 1 return retrn(rows, cols, X), reclen def _rd_nonbigmat_binary( self, fp, wper, r, c, rows, cols, nwords, reclen, bytesreal, numform, numform2, funcs, ): init, put, retrn = funcs X = init(rows, cols) cutoff, s3, b3, s4 = self._get_cutoff_etc() s1, b1 = self._get_s1() while c < cols: # non-bigmat sparse format # Read column data, one string of numbers at a time # (strings of zeros are skipped) while nwords > 0: IS = s1(fp.read(b1))[0] L = (IS >> 16) - 1 # L r = IS - ((L + 1) << 16) - 1 # irow-1 nwords -= L + 1 # words left L //= wper if L < cutoff: Y = struct.unpack(numform % L, fp.read(bytesreal * L)) else: Y = np.fromfile(fp, numform2, L) put(X, r, c, Y) fp.read(4) reclen = s4(fp.read(4))[0] c, r, nwords = s3(fp.read(b3)) c -= 1 return retrn(rows, cols, X), reclen def _loadop4_binary(self, patternlist=None, listonly=False, sparse=False): """ Reads next matching matrix or returns information on the next matrix in the binary op4 file. Parameters ---------- patternlist : list List of string patterns; each matrix name is matched against this list: if it matches any of the patterns, it is read in. listonly : bool True if only reading name. sparse : bool or None or two-tuple_like; optional Specifies whether output matrices will be regular numpy arrays or sparse arrays. If not two-tuple_like: ======== =============================================== `sparse` Action ======== =============================================== None Auto setting: each matrix will be sparse if and only if it was written in a sparse format True Matrices will be returned in sparse format False Matrices will be returned in regular (dense) numpy arrays ======== =============================================== If `sparse` is two-tuple_like, the first element is either None, True, or False (see table above) and the second element is a callable, as in: ``X = callable(X)``. A common usage of the callable would be to convert from "COO" sparse form (see :class:`scipy.sparse.coo_matrix`) to a more desirable form. For example, to ensure *all* matrices are returned in CSC form (see :class:`scipy.sparse.csc_matrix`) use:: sparse=(True, scipy.sparse.coo_matrix.tocsc) The callable is ignored for non-sparse matrices. Returns ------- tuple: (name, matrix, form, mtype) name : string Lower-case name of matrix. matrix : 2d ndarray The matrix. form : integer Nastran form of matrix. mtype : integer Nastran matrix type. Notes: - All outputs will be None if reached EOF. - The `matrix` output will be [rows, cols] of the matrix if the matrix is skipped. """ fp = self._fileh while 1: if len(fp.read(4)) == 0: return None, None, None, None cols, rows, form, mtype = self._Str_iiii.unpack(fp.read(self._bytes_iiii)) # Read ascii name of matrix: if self._bit64: name = fp.read(16).decode() else: name = fp.read(8).decode() name = self._check_name(name) fp.read(4) if patternlist and name not in patternlist: skip = 1 else: skip = 0 if listonly or skip: self._skipop4_binary(cols) if listonly: return name, (abs(rows), cols), form, mtype else: break reclen = self._Str_i4.unpack(fp.read(4))[0] c, r, nwords = self._Str_iii.unpack(fp.read(self._bytes_iii)) c -= 1 sparse, sparsefunc = OP4._get_sparsefunc(sparse) rdfunc, funcs = self._get_funcs("binary", rows, r, mtype, sparse, c >= cols) if mtype & 1: numform = self._str_sr numform2 = self._str_sr_fromfile bytesreal = self._bytes_sr wper = 1 else: numform = self._str_dr numform2 = self._str_dr_fromfile bytesreal = 8 wper = self._wordsperdouble # should this be 2 no matter what? X, reclen = rdfunc( fp, wper, r, c, abs(rows), cols, nwords, reclen, bytesreal, numform, numform2, funcs, ) if sparsefunc and sp.issparse(X): X = sparsefunc(X) # read final bytes of record and record marker nbytes = reclen - 3 * self._bytes_i + 4 if len(fp.read(nbytes)) < nbytes: warnings.warn( f"Premature end-of-file after matrix {name!r}. Nastran " "will likely FATAL on this file.", RuntimeWarning, ) return name, X, form, mtype @staticmethod def _sparse_col_stats(r): """ Returns locations of non-zero values and length of each series. Parameters ---------- r : ndarray 1d ndarray containing the indices of the values in the data column Returns ------- ind : ndarray m x 2 ndarray. m is number of non-zero sequences in v. First column contains the indices to the start of each sequence and the second column contains the length of the sequence. For example, if v is:: v = [ 0., 0., 0., 7., 5., 0., 6., 0., 2., 3.] which makes r be:: r = [ 3, 4, 6, 8, 9 ] Then, ind will be:: ind = [[3 2] [6 1] [8 2]] """ dr = np.diff(r) starts = np.nonzero(dr != 1)[0] + 1 nrows = len(starts) + 1 ind = np.zeros((nrows, 2), int) ind[0, 0] = r[0] if nrows > 1: ind[1:, 0] = r[starts] ind[0, 1] = starts[0] - 1 ind[1:-1, 1] = np.diff(starts) - 1 ind[-1, 1] = len(dr) - len(starts) - sum(ind[:, 1]) ind[:, 1] += 1 return ind # @staticmethod # def _is_symmetric(m, tol=1e-12): # """ # returns True if `m` is approx symmetric; sparse or not # """ # return abs(m - m.transpose()).max() <= tol * abs(m).max() @staticmethod def _is_symmetric(m): """ Returns True if matrix `m` is approx symmetric. Works for sparse and non-sparse matrices. Only called for square matrices, so that check is not done here. Note: if m is sparse, what gets input to the routine is a tuple: (m, r, c, v) where m is the original sparse matrix and r, c, v is the output from scipy.sparse.find(m). """ if isinstance(m, tuple): r, c, v = m[1:] low = r > c # values in lower triangle upp = c > r # values in upper triangle if np.count_nonzero(low) != np.count_nonzero(upp): return False rl = r[low] cl = c[low] vl = v[low] ru = r[upp] cu = c[upp] vu = v[upp] sortl = np.lexsort((cl, rl)) sortu = np.lexsort((ru, cu)) return ( np.all(cl[sortl] == ru[sortu]) and np.all(rl[sortl] == cu[sortu]) and np.allclose(vl[sortl], vu[sortu]) ) return np.allclose(m.transpose(), m) @staticmethod def _sparse_sort(matrix): # sparse matrix: m, r, c, v = matrix pv = np.lexsort((r, c)) # sort by column, then by row rs = r[pv] cs = c[pv] vs = v[pv] cols_with_data = sorted(set(cs)) return rs, cs, vs, cols_with_data @staticmethod def _get_header_info(matrix, form=None, is_ascii=False): if isinstance(matrix, tuple): mat = matrix[0] vals = matrix[3] else: mat = vals = matrix rows, cols = mat.shape if is_ascii: # if rows is more than 8 digits or cols + 1 is more than 8 # digits, raise error if rows > 99_999_999 or cols > 99_999_998: raise ValueError( "current maximum matrix dimensions for ascii writes are:" f" (99999999, 99999998). Have: {mat.shape}." ) # if rows is more than 7 digits (allowing one extra space # for minus sign on the bigmat format), then use 16 # characters for integers for the header int_width = 16 if rows > 9_999_999 else 8 else: # if rows or cols are > 2147483647 (2**31 - 1), raise error if rows > 2_147_483_647 or cols > 2_147_483_647: raise ValueError( "current maximum matrix dimensions for binary writes are:" f" ({2**31-1}, {2**31-1}). Have: {mat.shape}." ) int_width = 4 if form is None: if rows == cols: if OP4._is_symmetric(matrix): form = 6 else: form = 1 else: form = 2 if np.iscomplexobj(vals): mtype = 4 multiplier = 2 else: mtype = 2 multiplier = 1 return rows, cols, form, mtype, multiplier, int_width def _write_ascii_header(self, f, name, matrix, digits, bigmat, form): """ Utility routine that writes the header for ascii matrices. Parameters ---------- f : file handle Output of open() using binary mode. name : string Name of matrix. matrix : matrix Matrix to write. digits : integer Number of significant digits after the decimal to include in the ascii output. bigmat : bool If true, matrix is to be written in 'bigmat' format. form : integer or None The matrix form. If None, the form will be determined automatically. Returns ------- tuple: (cols, multiplier, perline, numlen, numform) cols : integer Number of columns in matrix. multiplier : integer 2 for complex, 1 for real. perline : integer Number of values written per row. numlen : integer Number of characters per value. numform : string Format string for numbers, eg: '%16.9E'. """ numlen = digits + 5 + self._expdigits # -1.digitsE-009 perline = 80 // numlen (rows, cols, form, mtype, multiplier, int_width) = OP4._get_header_info( matrix, form, True, ) if bigmat: rows = -rows addon = "|I16" if int_width == 16 else "" f.write( f"{cols:{int_width}}{rows:{int_width}}{form:8}{mtype:8}{name.upper():8s}" f"1P,{perline}E{numlen}.{digits}{addon}\n" ) numform = f"%{numlen}.{digits}E" return cols, multiplier, perline, numlen, numform def _write_ascii(self, f, name, matrix, digits, form): """ Write a matrix to a file in ascii, non-sparse format. Parameters ---------- f : file handle Output of open() using text mode. name : string Name of matrix. matrix : matrix Matrix to write. digits : integer Number of significant digits after the decimal to include in the ascii output. form : integer or None The matrix form. If None, the form will be determined automatically. """ (cols, multiplier, perline, numlen, numform) = self._write_ascii_header( f, name, matrix, digits, bigmat=False, form=form ) def _write_col_data(f, v, c, s, elems, perline, numform): f.write(f"{c + 1:8}{s + 1:8}{elems:8}\n") neven = ((elems - 1) // perline) * perline for i in range(0, neven, perline): for j in range(perline): f.write(numform % v[i + j]) f.write("\n") for i in range(neven, elems): f.write(numform % v[i]) f.write("\n") if isinstance(matrix, np.ndarray): for c in range(cols): v = matrix[:, c] if np.any(v): pv = np.nonzero(v)[0] s = pv[0] e = pv[-1] elems = (e - s + 1) * multiplier v = np.asarray(v[s : e + 1]).ravel() v.dtype = float _write_col_data(f, v, c, s, elems, perline, numform) else: # sparse matrix: rs, cs, vs, cols_with_data = OP4._sparse_sort(matrix) dt = float if multiplier == 1 else complex for c in cols_with_data: pv = (cs == c).nonzero()[0] # find data for column c s = rs[pv[0]] # first row with value e = rs[pv[-1]] # last row with value elems = e - s + 1 vec = np.zeros(elems, dt) vec[rs[pv] - s] = vs[pv] elems *= multiplier vec.dtype = float _write_col_data(f, vec, c, s, elems, perline, numform) f.write(f"{cols + 1:8}{1:8}{1:8}\n") f.write(numform % 2**0.5) f.write("\n") @staticmethod def _write_ascii_sparse( f, matrix, cols, _write_col_header, _write_data_string, multiplier, perline, numform, ): if isinstance(matrix, np.ndarray): for c in range(cols): v = matrix[:, c] if np.any(v): v = np.asarray(v).ravel() ind = OP4._sparse_col_stats(v.nonzero()[0]) _write_col_header(f, ind, c, multiplier) for r0, r1 in ind: string = v[r0 : r0 + r1] string.dtype = float _write_data_string( f, string, r0, r1, multiplier, perline, numform ) else: # sparse matrix: rs, cs, vs, cols_with_data = OP4._sparse_sort(matrix) for c in cols_with_data: pv = (cs == c).nonzero()[0] # find data for column c ind = OP4._sparse_col_stats(rs[pv]) _write_col_header(f, ind, c, multiplier) coldata = vs[pv] j = 0 for r0, r1 in ind: string = coldata[j : j + r1] j += r1 string.dtype = float _write_data_string(f, string, r0, r1, multiplier, perline, numform) f.write(f"{cols + 1:8}{1:8}{1:8}\n") f.write(numform % 2**0.5) f.write("\n") def _write_ascii_nonbigmat(self, f, name, matrix, digits, form): """ Write a matrix to a file in ascii, non-bigmat sparse format. Parameters ---------- f : file handle Output of open() using binary mode. name : string Name of matrix. matrix : matrix Matrix to write. digits : integer Number of significant digits after the decimal to include in the ascii output. form : integer or None The matrix form. If None, the form will be determined automatically. Note: if rows > 65535, bigmat is turned on and the :func:`write_ascii_bigmat` function is called ... that's a Nastran rule. """ if isinstance(matrix, tuple): rows, cols = matrix[0].shape else: rows, cols = matrix.shape if rows >= self._rows4bigmat: self._write_ascii_bigmat(f, name, matrix, digits, form) return (cols, multiplier, perline, numlen, numform) = self._write_ascii_header( f, name, matrix, digits, bigmat=False, form=form ) def _write_col_header(f, ind, c, multiplier): nwords = ind.shape[0] + 2 * sum(ind[:, 1]) * multiplier f.write(f"{c + 1:8}{0:8}{nwords:8}\n") def _write_data_string(f, string, r0, r1, multiplier, perline, numform): L = r1 * 2 * multiplier IS = (r0 + 1) + ((L + 1) << 16) f.write(f"{IS:11}\n") elems = L // 2 neven = ((elems - 1) // perline) * perline for i in range(0, neven, perline): for j in range(perline): f.write(numform % string[i + j]) f.write("\n") for i in range(neven, elems): f.write(numform % string[i]) f.write("\n") OP4._write_ascii_sparse( f, matrix, cols, _write_col_header, _write_data_string, multiplier, perline, numform, ) def _write_ascii_bigmat(self, f, name, matrix, digits, form): """ Write a matrix to a file in ascii, bigmat sparse format. Parameters ---------- f : file handle Output of open() using binary mode. name : string Name of matrix. matrix : matrix Matrix to write. digits : integer Number of significant digits after the decimal to include in the ascii output. form : integer or None The matrix form. If None, the form will be determined automatically. """ (cols, multiplier, perline, numlen, numform) = self._write_ascii_header( f, name, matrix, digits, bigmat=True, form=form ) def _write_col_header(f, ind, c, multiplier): nwords = 2 * ind.shape[0] + 2 * sum(ind[:, 1]) * multiplier f.write(f"{c + 1:8}{0:8}{nwords:8}\n") def _write_data_string(f, string, r0, r1, multiplier, perline, numform): L = r1 * 2 * multiplier f.write(f"{L + 1:8}{r0 + 1:8}\n") elems = L // 2 neven = ((elems - 1) // perline) * perline for i in range(0, neven, perline): for j in range(perline): f.write(numform % string[i + j]) f.write("\n") for i in range(neven, elems): f.write(numform % string[i]) f.write("\n") OP4._write_ascii_sparse( f, matrix, cols, _write_col_header, _write_data_string, multiplier, perline, numform, ) def _write_binary_header(self, f, name, matrix, endian, bigmat, form): """ Utility routine that writes the header for binary matrices. Parameters ---------- f : file handle Output of open() using binary mode. name : string Name of matrix. matrix : matrix Matrix to write. endian : string Endian setting for binary output: '=' for native, '>' for big-endian and '<' for little-endian. bigmat : bool If true, matrix is to be written in 'bigmat' format. form : integer or None The matrix form. If None, the form will be determined automatically. Returns ------- tuple: (cols, multiplier) cols : integer Number of columns in matrix. multiplier : integer 2 for complex, 1 for real. """ (rows, cols, form, mtype, multiplier, int_width) = OP4._get_header_info( matrix, form ) # write 1st record (24 bytes: 4 4-byte ints, 1 8-byte string) name = (f"{name.upper():<8}").encode() if bigmat: # ~~ if rows < self._rows4bigmat: rows = -rows f.write(struct.pack(endian + "5i8si", 24, cols, rows, form, mtype, name, 24)) return cols, multiplier def _write_binary(self, f, name, matrix, endian, form): """ Write a matrix to a file in double precision binary format. Parameters ---------- f : file handle Output of open() using binary mode. name : string Name of matrix. matrix : matrix Matrix to write. endian : string Endian setting for binary output: '=' for native, '>' for big-endian and '<' for little-endian. form : integer or None The matrix form. If None, the form will be determined automatically. """ cols, multiplier = self._write_binary_header( f, name, matrix, endian, bigmat=False, form=form ) def _write_col_data(f, v, c, s, elems, endian, colHeader, colTrailer): reclen = 3 * 4 + elems * 8 f.write(colHeader.pack(reclen, c + 1, s + 1, 2 * elems)) f.write(struct.pack(endian + ("%dd" % elems), *v)) f.write(colTrailer.pack(reclen)) colHeader = struct.Struct(endian + "4i") colTrailer = struct.Struct(endian + "i") if isinstance(matrix, np.ndarray): for c in range(cols): v = matrix[:, c] if np.any(v): pv = np.nonzero(v)[0] s = pv[0] e = pv[-1] elems = (e - s + 1) * multiplier v = np.asarray(v[s : e + 1]).ravel() v.dtype = float _write_col_data(f, v, c, s, elems, endian, colHeader, colTrailer) else: # sparse matrix: rs, cs, vs, cols_with_data = OP4._sparse_sort(matrix) dt = float if multiplier == 1 else complex for c in cols_with_data: pv = (cs == c).nonzero()[0] # find data for column c s = rs[pv[0]] # first row with value e = rs[pv[-1]] # last row with value elems = e - s + 1 vec = np.zeros(elems, dt) vec[rs[pv] - s] = vs[pv] elems *= multiplier vec.dtype = float _write_col_data(f, vec, c, s, elems, endian, colHeader, colTrailer) reclen = 3 * 4 + 8 f.write(colHeader.pack(reclen, cols + 1, 1, 2)) f.write(struct.pack(endian + "d", 2**0.5)) f.write(colTrailer.pack(reclen)) @staticmethod def _write_binary_sparse( f, matrix, cols, _write_col_header, _write_data_string, multiplier, colHeader, colTrailer, LrStruct, endian, ): if isinstance(matrix, np.ndarray): for c in range(cols): v = matrix[:, c] if np.any(v): v = np.asarray(v).ravel() ind = OP4._sparse_col_stats(v.nonzero()[0]) reclen = _write_col_header(f, ind, c, multiplier, colHeader) for r0, r1 in ind: string = v[r0 : r0 + r1] string.dtype = float _write_data_string( f, string, r0, r1, multiplier, LrStruct, endian ) f.write(colTrailer.pack(reclen)) else: # sparse matrix: rs, cs, vs, cols_with_data = OP4._sparse_sort(matrix) for c in cols_with_data: pv = (cs == c).nonzero()[0] # find data for column c ind = OP4._sparse_col_stats(rs[pv]) reclen = _write_col_header(f, ind, c, multiplier, colHeader) coldata = vs[pv] j = 0 for r0, r1 in ind: string = coldata[j : j + r1] j += r1 string.dtype = float _write_data_string(f, string, r0, r1, multiplier, LrStruct, endian) f.write(colTrailer.pack(reclen)) reclen = 3 * 4 + 8 f.write(colHeader.pack(reclen, cols + 1, 1, 2)) f.write(struct.pack(endian + "d", 2**0.5)) f.write(colTrailer.pack(reclen)) def _write_binary_nonbigmat(self, f, name, matrix, endian, form): """ Write a matrix to a file in double precision binary, non-bigmat sparse format. Parameters ---------- f : file handle Output of open() using binary mode. name : string Name of matrix. matrix : matrix Matrix to write. endian : string Endian setting for binary output: '=' for native, '>' for big-endian and '<' for little-endian. form : integer or None The matrix form. If None, the form will be determined automatically. Note: if rows > 65535, bigmat is turned on and the :func:`write_binary_bigmat` function is called ... that's a Nastran rule. """ if isinstance(matrix, tuple): rows, cols = matrix[0].shape else: rows, cols = matrix.shape if rows >= self._rows4bigmat: self._write_binary_bigmat(f, name, matrix, endian, form) return cols, multiplier = self._write_binary_header( f, name, matrix, endian, bigmat=False, form=form ) colHeader = struct.Struct(endian + "4i") colTrailer = struct.Struct(endian + "i") def _write_col_header(f, ind, c, multiplier, colHeader): nwords = ind.shape[0] + 2 * sum(ind[:, 1]) * multiplier reclen = (3 + nwords) * 4 f.write(colHeader.pack(reclen, c + 1, 0, nwords)) return reclen def _write_data_string(f, string, r0, r1, multiplier, colTrailer, endian): L = r1 * 2 * multiplier IS = (r0 + 1) + ((L + 1) << 16) f.write(colTrailer.pack(IS)) f.write(struct.pack(endian + ("%dd" % len(string)), *string)) OP4._write_binary_sparse( f, matrix, cols, _write_col_header, _write_data_string, multiplier, colHeader, colTrailer, colTrailer, endian, ) def _write_binary_bigmat(self, f, name, matrix, endian, form): """ Write a matrix to a file in double precision binary, bigmat sparse format. Parameters ---------- f : file handle Output of open() using binary mode. name : string Name of matrix. matrix : matrix Matrix to write. endian : string Endian setting for binary output: '=' for native, '>' for big-endian and '<' for little-endian. form : integer or None The matrix form. If None, the form will be determined automatically. """ cols, multiplier = self._write_binary_header( f, name, matrix, endian, bigmat=True, form=form ) colHeader = struct.Struct(endian + "4i") colTrailer = struct.Struct(endian + "i") LrStruct = struct.Struct(endian + "ii") def _write_col_header(f, ind, c, multiplier, colHeader): nwords = 2 * ind.shape[0] + 2 * sum(ind[:, 1]) * multiplier reclen = (3 + nwords) * 4 f.write(colHeader.pack(reclen, c + 1, 0, nwords)) return reclen def _write_data_string(f, string, r0, r1, multiplier, LrStruct, endian): L = r1 * 2 * multiplier f.write(LrStruct.pack(L + 1, r0 + 1)) f.write(struct.pack(endian + ("%dd" % len(string)), *string)) OP4._write_binary_sparse( f, matrix, cols, _write_col_header, _write_data_string, multiplier, colHeader, colTrailer, LrStruct, endian, )
[docs] def dctload(self, filename, namelist=None, justmatrix=False, sparse=False): """ Read all matching matrices from op4 file into dictionary. Parameters ---------- filename : string Name of op4 file to read. namelist : list, string, or None; optional List of variable names to read in, or string with name of the single variable to read in, or None. If None, all matrices are read in. justmatrix : bool; optional If True, only the matrix is stored in the dictionary. If False, a tuple of ``(matrix, form, mtype)`` is stored. sparse : bool or None or two-tuple_like; optional Specifies whether output matrices will be regular numpy arrays or sparse arrays. If not two-tuple_like: ======== =============================================== `sparse` Action ======== =============================================== None Auto setting: each matrix will be sparse if and only if it was written in a sparse format True Matrices will be returned in sparse format False Matrices will be returned in regular (dense) numpy arrays ======== =============================================== If `sparse` is two-tuple_like, the first element is either None, True, or False (see table above) and the second element is a callable, as in: ``X = callable(X)``. A common usage of the callable would be to convert from "COO" sparse form (see :class:`scipy.sparse.coo_matrix`) to a more desirable form. For example, to ensure *all* matrices are returned in CSC form (see :class:`scipy.sparse.csc_matrix`) use:: sparse=(True, scipy.sparse.coo_matrix.tocsc) The callable is ignored for non-sparse matrices. Returns ------- dct : :class:`collections.OrderedDict` Keys are the lower-case matrix names and the values are either just the matrix or a tuple of: ``(matrix, form, mtype)`` depending on `justmatrix`. Notes ----- The default form for sparse matrices is the "COO" sparse form (see :class:`scipy.sparse.coo_matrix`). To override, provide a callable in the `sparse` option (see above). See also -------- :func:`listload`, :func:`write` (or :func:`save`), :func:`dir`. """ if isinstance(namelist, str): namelist = [namelist] self._op4open_read(filename) dct = collections.OrderedDict() try: if self._ascii: loadfunc = self._loadop4_ascii else: loadfunc = self._loadop4_binary while 1: name, X, form, mtype = loadfunc(patternlist=namelist, sparse=sparse) if not name: break if justmatrix: dct[name] = X else: dct[name] = X, form, mtype finally: self._op4close() return dct
[docs] def listload(self, filename, namelist=None, sparse=False): """ Read all matching matrices from op4 file into a list; useful if op4 file has duplicate names. Parameters ---------- filename : string Name of op4 file to read. namelist : list, string, or None; optional List of variable names to read in, or string with name of the single variable to read in, or None. If None, all matrices are read in. sparse : bool or None or two-tuple_like; optional Specifies whether output matrices will be regular numpy arrays or sparse arrays. If not two-tuple_like: ======== =============================================== `sparse` Action ======== =============================================== None Auto setting: each matrix will be sparse if and only if it was written in a sparse format True Matrices will be returned in sparse format False Matrices will be returned in regular (dense) numpy arrays ======== =============================================== If `sparse` is two-tuple_like, the first element is either None, True, or False (see table above) and the second element is a callable, as in: ``X = callable(X)``. A common usage of the callable would be to convert from "COO" sparse form (see :class:`scipy.sparse.coo_matrix`) to a more desirable form. For example, to ensure *all* matrices are returned in CSC form (see :class:`scipy.sparse.csc_matrix`) use:: sparse=(True, scipy.sparse.coo_matrix.tocsc) The callable is ignored for non-sparse matrices. Returns ------- names : list Lower-case list of matrix names in order as read. matrices : list List of matrices in order as read. forms : list List of integers specifying the Nastran form of each matrix. mtypes : list List of integers specifying the Nastran type of each matrix. Notes ----- The default form for sparse matrices is the "COO" sparse form (see :class:`scipy.sparse.coo_matrix`). To override, provide a callable in the `sparse` option (see above). See also -------- :func:`dctload`, :func:`write` (or :func:`save`), :func:`dir`. """ if isinstance(namelist, str): namelist = [namelist] self._op4open_read(filename) names = [] matrices = [] forms = [] mtypes = [] try: if self._ascii: loadfunc = self._loadop4_ascii else: loadfunc = self._loadop4_binary while 1: name, X, form, mtype = loadfunc(patternlist=namelist, sparse=sparse) if not name: break names.append(name) matrices.append(X) forms.append(form) mtypes.append(mtype) finally: self._op4close() return names, matrices, forms, mtypes
[docs] def load(self, filename, namelist=None, into="dct", justmatrix=False, sparse=False): """ Read all matching matrices from op4 file into dictionary or list; interface to :func:`dctload` and :func:`listload`. Parameters ---------- filename : string Name of op4 file to read. namelist : list, string, or None; optional List of variable names to read in, or string with name of the single variable to read in, or None. If None, all matrices are read in. into : string; optional Either 'dct' or 'list'. Use 'list' if multiple matrices share the same name. See below. justmatrix : bool; optional If True, only the matrix is stored in the dictionary. If False, a tuple of ``(matrix, form, mtype)`` is stored. This option is ignored if ``into == 'list'``. sparse : bool or None or two-tuple_like; optional Specifies whether output matrices will be regular numpy arrays or sparse arrays. If not two-tuple_like: ======== =============================================== `sparse` Action ======== =============================================== None Auto setting: each matrix will be sparse if and only if it was written in a sparse format True Matrices will be returned in sparse format False Matrices will be returned in regular (dense) numpy arrays ======== =============================================== If `sparse` is two-tuple_like, the first element is either None, True, or False (see table above) and the second element is a callable, as in: ``X = callable(X)``. A common usage of the callable would be to convert from "COO" sparse form (see :class:`scipy.sparse.coo_matrix`) to a more desirable form. For example, to ensure *all* matrices are returned in CSC form (see :class:`scipy.sparse.csc_matrix`) use:: sparse=(True, scipy.sparse.coo_matrix.tocsc) The callable is ignored for non-sparse matrices. Returns ------- dct : :class:`collections.OrderedDict`, if ``into == 'dct'`` Keys are the lower-case matrix names and the values are either just the matrix or a tuple of: ``(matrix, form, mtype)`` depending on `justmatrix`. tup : tuple, if ``into == 'list'`` Tuple of 4 lists: ``(names, matrices, forms, mtypes)`` Notes ----- The default form for sparse matrices is the "COO" sparse form (see :class:`scipy.sparse.coo_matrix`). To override, provide a callable in the `sparse` option (see above). See also -------- :func:`write` (or :func:`save`), :func:`dir`. """ if into == "dct": return self.dctload(filename, namelist, justmatrix, sparse) elif into == "list": return self.listload(filename, namelist, sparse) raise ValueError('invalid "into" option')
[docs] def dir(self, filename, verbose=True): """ Directory of all matrices in op4 file. Parameters ---------- filename : string Name of op4 file to read. verbose : bool; optional If true, directory will be printed to screen. Returns ------- names : list Lower-case list of matrix names in order as read. sizes : list List of sizes [(r1, c1), (r2, c2), ...], for each matrix. forms : list List of integers specifying the Nastran form of each matrix. mtypes : list List of integers specifying the Nastran type of each matrix. See also -------- :func:`dctload`, :func:`listload`, :func:`write` (or :func:`save`). """ self._op4open_read(filename) names = [] sizes = [] forms = [] mtypes = [] try: if self._ascii: loadfunc = self._loadop4_ascii else: loadfunc = self._loadop4_binary while 1: name, X, form, mtype = loadfunc(listonly=True) if not name: break names.append(name) sizes.append(X) forms.append(form) mtypes.append(mtype) if verbose: for n, s, f, m in zip(names, sizes, forms, mtypes): print(f"{n:8}, {s[0]:6} x {s[1]:<6}, form={f}, mtype={m}") finally: self._op4close() return names, sizes, forms, mtypes
[docs] def write( self, filename, names, matrices=None, binary=True, digits=16, endian="=", sparse="auto", forms=None, ): """ Write op4 file. Parameters ---------- filename : string Name of file. names : dictionary/OrderedDict or list or string Dictionary indexed by the matrix names with the values being either the matrices or a tuple/list where the first two items are ``(matrix, form)``. Alternatively, `names` can also be a list of matrix names (strings) or a single name (string) if just one matrix is to be saved. If using the list inputs, `matrices` is required to specify the matrices and, if `forms` needs to specified, that input would also be required. matrices : array or list; optional 2d ndarray or list of 2d ndarrays. Ignored if `names` is a dictionary. Same length as `names`. binary : bool; optional If true, a double precision binary file is written; otherwise an ascii file is created. digits : integer; optional Number of significant digits after the decimal to include in the ascii output. Ignored for binary files. endian : string; optional Endian setting for binary output: '=' for native, '>' for big-endian and '<' for little-endian. sparse : string; optional Specifies the output format: =========== =========================================== `sparse` Action =========== =========================================== 'auto' Each 2d ndarray will be written in "dense" format and each sparse matrix will be written in "bigmat" sparse format 'bigmat' Each matrix (whether sparse or not) is written in "bigmat" sparse format 'dense' Each matrix will be written in "dense" format 'nonbigmat' Each matrix is written in "nonbigmat" format. Note that if the number of rows is > 65535, then the "bigmat" format is used. =========== =========================================== forms : integer or list or None; optional The matrix form(s). If None, the forms will be determined automatically to be 1, 2, or 6. Ignored if `names` is a dictionary (which would contain this information). If not None, `forms` must be the same length as `names`, but you can use None as the form for one or more matrices (see example in :func:`pyyeti.nastran.op4.write`). From Nastran documentation: ====== ============================== form Matrix format ====== ============================== 1 Square 2 Rectangular 3 Diagonal 4 Lower triangular factor 5 Upper triangular factor 6 Symmetric 8 Identity 9 Pseudo identity 10 Cholesky factor 11 Trapezoidal factor 13 Sparse lower triangular factor 15 Sparse upper triangular factor ====== ============================== .. warning:: The validity of the values in `forms` is not checked in any way. Returns ------- None. Notes ----- To write multiple matrices that have the same name, `names` must be a list, not a dictionary. If a list, the order is maintained. If a dictionary, the matrices are written in the order they are retrieved from the dictionary; use a :class:`collections.OrderedDict` to specify a certain order. See the examples in :func:`pyyeti.nastran.op4.write`. See also -------- :func:`pyyeti.nastran.op4.write`, :func:`dctload`, :func:`listload`, :func:`dir`. """ if isinstance(names, Mapping): _names = [] matrices = [] forms = [] for nm, val in names.items(): if isinstance(val, (list, tuple)): _names.append(nm) matrices.append(val[0]) forms.append(val[1]) else: _names.append(nm) matrices.append(val) forms.append(None) names = _names else: if not isinstance(names, (list, tuple)): names = [names] if not isinstance(matrices, (list, tuple)): matrices = [matrices] if forms is not None and not isinstance(forms, (list, tuple)): forms = [forms] if forms is None: forms = [None] * len(names) matrices = [_ensure_2d_dp(matrix) for matrix in matrices] names = _check_write_names(names) if binary: if sparse == "dense": wrtfunc = self._write_binary elif sparse == "bigmat": wrtfunc = self._write_binary_bigmat elif sparse == "nonbigmat": wrtfunc = self._write_binary_nonbigmat elif sparse != "auto": raise ValueError("invalid sparse option") if endian == "": endian = "=" # for backwards compatibility with open(filename, "wb") as f: for name, matrix, form in zip(names, matrices, forms): if sparse == "auto": if isinstance(matrix, tuple): wrtfunc = self._write_binary_bigmat else: wrtfunc = self._write_binary wrtfunc(f, name, matrix, endian, form) else: if sparse == "dense": wrtfunc = self._write_ascii elif sparse == "bigmat": wrtfunc = self._write_ascii_bigmat elif sparse == "nonbigmat": wrtfunc = self._write_ascii_nonbigmat elif sparse != "auto": raise ValueError("invalid sparse option") with open(filename, "w") as f: for name, matrix, form in zip(names, matrices, forms): if sparse == "auto": if isinstance(matrix, tuple): wrtfunc = self._write_ascii_bigmat else: wrtfunc = self._write_ascii wrtfunc(f, name, matrix, digits, form)
[docs] def load(filename=None, namelist=None, into="dct", justmatrix=False, sparse=False): """ Read all matching matrices from op4 file into dictionary or list; non-member version of :func:`OP4.load`. This is a the same as :func:`read` except `justmatrix` default is False. Parameters ---------- filename : string or None; optional Name of op4 file to read. Can also be the name of a directory or None; in these cases, a GUI is opened for file selection. namelist : list, string, or None; optional List of variable names to read in, or string with name of the single variable to read in, or None. If None, all matrices are read in. into : string; optional Either 'dct' or 'list'. Use 'list' if multiple matrices share the same name. See below. justmatrix : bool; optional If True, only the matrix is stored in the dictionary. If False, a tuple of ``(matrix, form, mtype)`` is stored. This option is ignored if ``into == 'list'``. sparse : bool or None or two-tuple_like; optional Specifies whether output matrices will be regular numpy arrays or sparse arrays. If not two-tuple_like: ======== =============================================== `sparse` Action ======== =============================================== None Auto setting: each matrix will be sparse if and only if it was written in a sparse format True Matrices will be returned in sparse format False Matrices will be returned in regular (dense) numpy arrays ======== =============================================== If `sparse` is two-tuple_like, the first element is either None, True, or False (see table above) and the second element is a callable, as in: ``X = callable(X)``. A common usage of the callable would be to convert from "COO" sparse form (see :class:`scipy.sparse.coo_matrix`) to a more desirable form. For example, to ensure *all* matrices are returned in CSC form (see :class:`scipy.sparse.csc_matrix`) use:: sparse=(True, scipy.sparse.coo_matrix.tocsc) The callable is ignored for non-sparse matrices. Returns ------- dct : :class:`collections.OrderedDict`, if ``into == 'dct'`` Keys are the lower-case matrix names and the values are either just the matrix or a tuple of: ``(matrix, form, mtype)`` depending on `justmatrix`. tup : tuple, if ``into == 'list'`` Tuple of 4 lists: ``(names, matrices, forms, mtypes)`` Notes ----- The default form for sparse matrices is the "COO" sparse form (see :class:`scipy.sparse.coo_matrix`). To override, provide a callable in the `sparse` option (see above). Examples -------- This examples translates a sparse format binary op4 file to a simpler ascii format while preserving the matrix forms. First, create a file in "bigmat" sparse format and set the form on the "m" matrix to be symmetric (form=6): >>> import numpy as np >>> from pyyeti.nastran import op4 >>> m = np.array([[1, 2], [2.1, 3]]) >>> k = np.array([3, 5]) >>> b = np.array([4, 6]) >>> names = ['m', 'k', 'b'] >>> values = [eval(n) for n in names] >>> op4.write('mkb.op4', names, values, forms=[6, 2, 2], ... sparse='bigmat') Now, translate it to simple ascii, preserving the forms: >>> dct = op4.load('mkb.op4') >>> op4.write('mkb_ascii.op4', dct, binary=False) Check that the order and forms are the same: >>> _ = op4.dir('mkb.op4') m , 2 x 2 , form=6, mtype=2 k , 1 x 2 , form=2, mtype=2 b , 1 x 2 , form=2, mtype=2 >>> _ = op4.dir('mkb_ascii.op4') m , 2 x 2 , form=6, mtype=2 k , 1 x 2 , form=2, mtype=2 b , 1 x 2 , form=2, mtype=2 Clean up: >>> import os >>> os.remove('mkb.op4') >>> os.remove('mkb_ascii.op4') See also -------- :func:`read`, :func:`write` (or :func:`save`), :func:`dir`. """ filename = guitools.get_file_name(filename, read=True) if into == "dct": return OP4().dctload(filename, namelist, justmatrix, sparse) elif into == "list": return OP4().listload(filename, namelist, sparse) raise ValueError('invalid "into" option')
[docs] def read(filename=None, namelist=None, into="dct", justmatrix=True, sparse=False): """ Read all matching matrices from op4 file into dictionary or list; non-member version of :func:`OP4.load`. This is a the same as :func:`load` except `justmatrix` default is True. Parameters ---------- filename : string or None; optional Name of op4 file to read. Can also be the name of a directory or None; in these cases, a GUI is opened for file selection. namelist : list, string, or None; optional List of variable names to read in, or string with name of the single variable to read in, or None. If None, all matrices are read in. into : string; optional Either 'dct' or 'list'. Use 'list' if multiple matrices share the same name. See below. justmatrix : bool; optional If True, only the matrix is stored in the dictionary. If False, a tuple of ``(matrix, form, mtype)`` is stored. This option is ignored if ``into == 'list'``. sparse : bool or None or two-tuple_like; optional Specifies whether output matrices will be regular numpy arrays or sparse arrays. If not two-tuple_like: ======== =============================================== `sparse` Action ======== =============================================== None Auto setting: each matrix will be sparse if and only if it was written in a sparse format True Matrices will be returned in sparse format False Matrices will be returned in regular (dense) numpy arrays ======== =============================================== If `sparse` is two-tuple_like, the first element is either None, True, or False (see table above) and the second element is a callable, as in: ``X = callable(X)``. A common usage of the callable would be to convert from "COO" sparse form (see :class:`scipy.sparse.coo_matrix`) to a more desirable form. For example, to ensure *all* matrices are returned in CSC form (see :class:`scipy.sparse.csc_matrix`) use:: sparse=(True, scipy.sparse.coo_matrix.tocsc) The callable is ignored for non-sparse matrices. Returns ------- dct : :class:`collections.OrderedDict`, if ``into == 'dct'`` Keys are the lower-case matrix names and the values are either just the matrix or a tuple of: ``(matrix, form, mtype)`` depending on `justmatrix`. tup : tuple, if ``into == 'list'`` Tuple of 4 lists: ``(names, matrices, forms, mtypes)`` Notes ----- The default form for sparse matrices is the "COO" sparse form (see :class:`scipy.sparse.coo_matrix`). To override, provide a callable in the `sparse` option (see above). See also -------- :func:`load`, :func:`write` (or :func:`save`), :func:`dir`. """ return load(filename, namelist, into, justmatrix, sparse)
[docs] def dir(filename=None, verbose=True): """ Directory of all matrices in op4 file; non-member version of :func:`OP4.dir`. Parameters ---------- filename : string or None; optional Name of op4 file to read. Can also be the name of a directory or None; in these cases, a GUI is opened for file selection. verbose : bool; optional If true, directory will be printed to screen. Returns ------- names : list Lower-case list of matrix names in order as read. sizes : list List of sizes [(r1, c1), (r2, c2), ...], for each matrix. forms : list List of integers specifying the Nastran form of each matrix. mtypes : list List of integers specifying the Nastran type of each matrix. See also -------- :func:`load`, :func:`write` (or :func:`save`). """ filename = guitools.get_file_name(filename, read=True) return OP4().dir(filename, verbose)
[docs] def write( filename, names, matrices=None, binary=True, digits=16, endian="=", sparse="auto", forms=None, ): """ Write op4 file; non-member version of :func:`OP4.write`. Parameters ---------- filename : string or None Name of file. Can also be the name of a directory or None; in these cases, a GUI is opened for file selection. names : dictionary/OrderedDict or list or string Dictionary indexed by the matrix names with the values being either the matrices or a tuple/list where the first two items are ``(matrix, form)``. Alternatively, `names` can also be a list of matrix names (strings) or a single name (string) if just one matrix is to be saved. If using the list inputs, `matrices` is required to specify the matrices and, if `forms` needs to specified, that input would also be required. matrices : array or list; optional 2d ndarray or list of 2d ndarrays. Ignored if `names` is a dictionary. Same length as `names`. binary : bool; optional If true, a double precision binary file is written; otherwise an ascii file is created. digits : integer; optional Number of significant digits after the decimal to include in the ascii output. Ignored for binary files. endian : string; optional Endian setting for binary output: '=' for native, '>' for big-endian and '<' for little-endian. sparse : string; optional Specifies the output format: =========== =========================================== `sparse` Action =========== =========================================== 'auto' Each 2d ndarray will be written in "dense" format and each sparse matrix will be written in "bigmat" sparse format 'bigmat' Each matrix (whether sparse or not) is written in "bigmat" sparse format 'dense' Each matrix will be written in "dense" format 'nonbigmat' Each matrix is written in "nonbigmat" format. Note that if the number of rows is > 65535, then the "bigmat" format is used. =========== =========================================== forms : integer or list or None; optional The matrix form(s). If None, the forms will be determined automatically to be 1, 2, or 6. Ignored if `names` is a dictionary (which would contain this information). If not None, `forms` must be the same length as `names`, but you can use None as the form for one or more matrices (see example below). From Nastran documentation: ====== ============================== form Matrix format ====== ============================== 1 Square 2 Rectangular 3 Diagonal 4 Lower triangular factor 5 Upper triangular factor 6 Symmetric 8 Identity 9 Pseudo identity 10 Cholesky factor 11 Trapezoidal factor 13 Sparse lower triangular factor 15 Sparse upper triangular factor ====== ============================== .. warning:: The validity of the values in `forms` is not checked in any way. Returns ------- None. Notes ----- To write multiple matrices that have the same name, `names` must be a list, not a dictionary. If a list, the order is maintained. If a dictionary, the matrices are written in the order they are retrieved from the dictionary; use a :class:`collections.OrderedDict` to specify a certain order. `save` is an alias for `write`. Examples -------- To write m, k, b, in that order to a binary file, you could use lists or an OrderedDict: >>> import numpy as np >>> from pyyeti.nastran import op4 >>> m = np.array([[1, 2], [2, 3]]) >>> k = np.array([3, 5]) >>> b = np.array([4, 6]) >>> names = ['m', 'k', 'b'] >>> values = [eval(n) for n in names] >>> op4.write('mkb.op4', names, values) >>> _ = op4.dir('mkb.op4') m , 2 x 2 , form=6, mtype=2 k , 1 x 2 , form=2, mtype=2 b , 1 x 2 , form=2, mtype=2 Or, order is also maintained when using an OrderedDict: >>> from collections import OrderedDict >>> odct = OrderedDict() >>> for n in names: ... odct[n] = eval(n) >>> op4.write('mkb.op4', odct) >>> _ = op4.dir('mkb.op4') m , 2 x 2 , form=6, mtype=2 k , 1 x 2 , form=2, mtype=2 b , 1 x 2 , form=2, mtype=2 On the other hand, if you don't care about the order, you could use a regular dictionary. (In more recent versions of Python, this may behave like an OrderedDict.): >>> op4.write('mkb.op4', dict(m=m, k=k, b=b)) >>> _ = op4.dir('mkb.op4') # doctest: +SKIP m , 2 x 2 , form=6, mtype=2 k , 1 x 2 , form=2, mtype=2 b , 1 x 2 , form=2, mtype=2 To specify the forms, include the `forms` option in either the list approach or the dictionary approach. Here, we'll just say that "m" is square (not symmetric) for demonstration. The forms for "k" and "b" will be automatically detected. First, the list approach: >>> op4.write('mkb.op4', ['m', 'k', 'b'], [m, k, b], ... forms=[1, None, None]) >>> _ = op4.dir('mkb.op4') m , 2 x 2 , form=1, mtype=2 k , 1 x 2 , form=2, mtype=2 b , 1 x 2 , form=2, mtype=2 Next, the dictionary approach. Since the form is attached to each matrix, it is only required if not None: >>> odct = OrderedDict() >>> odct['m'] = (m, 1) >>> odct['k'] = k >>> odct['b'] = b >>> op4.write('mkb.op4', odct) >>> _ = op4.dir('mkb.op4') m , 2 x 2 , form=1, mtype=2 k , 1 x 2 , form=2, mtype=2 b , 1 x 2 , form=2, mtype=2 Clean up: >>> import os >>> os.remove('mkb.op4') See also -------- :func:`load`, :func:`dir`. """ filename = guitools.get_file_name(filename, read=False) OP4().write(filename, names, matrices, binary, digits, endian, sparse, forms)
# create `save` as an alias for `write` save = write