• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pypest / pyemu / 5887625428

17 Aug 2023 06:23AM UTC coverage: 79.857% (+1.5%) from 78.319%
5887625428

push

github

briochh
Merge branch 'develop'

11386 of 14258 relevant lines covered (79.86%)

6.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.45
/pyemu/mat/mat_handler.py
1
from __future__ import print_function, division
9✔
2
import os
9✔
3
import copy
9✔
4
import struct
9✔
5
import warnings
9✔
6
import numpy as np
9✔
7
import pandas as pd
9✔
8

9
# import scipy.linalg as la
10

11

12
from pyemu.pst.pst_handler import Pst
9✔
13
from ..pyemu_warnings import PyemuWarning
9✔
14

15

16
def save_coo(x, row_names, col_names, filename, chunk=None):
9✔
17
    """write a PEST-compatible binary file.  The data format is
18
    [int,int,float] for i,j,value.  It is autodetected during
19
    the read with Matrix.from_binary().
20

21
    Args:
22
        x (`numpy.sparse`): coo sparse matrix
23
        row_names ([`str`]): list of row names
24
        col_names (['str]): list of col_names
25
        filename (`str`):  filename
26
        droptol (`float`): absolute value tolerance to make values
27
            smaller than `droptol` zero.  Default is None (no dropping)
28
        chunk (`int`): number of elements to write in a single pass.
29
            Default is None
30

31
    """
32

33
    f = open(filename, "wb")
×
34
    # print("counting nnz")
35
    # write the header
36
    header = np.array((x.shape[1], x.shape[0], x.nnz), dtype=Matrix.binary_header_dt)
×
37
    header.tofile(f)
×
38

39
    data = np.core.records.fromarrays([x.row, x.col, x.data], dtype=Matrix.coo_rec_dt)
×
40
    data.tofile(f)
×
41

42
    for name in col_names:
×
43
        if len(name) > Matrix.new_par_length:
×
44
            name = name[: Matrix.new_par_length - 1]
×
45
        elif len(name) < Matrix.new_par_length:
×
46
            for _ in range(len(name), Matrix.new_par_length):
×
47
                name = name + " "
×
48
        f.write(name.encode())
×
49
    for name in row_names:
×
50
        if len(name) > Matrix.new_obs_length:
×
51
            name = name[: Matrix.new_obs_length - 1]
×
52
        elif len(name) < Matrix.new_obs_length:
×
53
            for i in range(len(name), Matrix.new_obs_length):
×
54
                name = name + " "
×
55
        f.write(name.encode())
×
56
    f.close()
×
57

58

59
def concat(mats):
9✔
60
    """Concatenate Matrix objects.  Tries either axis.
61

62
    Args:
63
        mats ([`Matrix`]): list of Matrix objects
64

65
    Returns:
66
        `pyemu.Matrix`: a concatenated `Matrix` instance
67
    """
68
    for mat in mats:
8✔
69
        if mat.isdiagonal:
8✔
70
            raise NotImplementedError("concat not supported for diagonal mats")
8✔
71

72
    row_match = True
8✔
73
    col_match = True
8✔
74
    for mat in mats[1:]:
8✔
75
        if sorted(mats[0].row_names) != sorted(mat.row_names):
8✔
76
            row_match = False
8✔
77
        if sorted(mats[0].col_names) != sorted(mat.col_names):
8✔
78
            col_match = False
8✔
79
    if not row_match and not col_match:
8✔
80
        raise Exception(
8✔
81
            "mat_handler.concat(): all Matrix objects"
82
            + "must share either rows or cols"
83
        )
84

85
    if row_match and col_match:
8✔
86
        raise Exception(
×
87
            "mat_handler.concat(): all Matrix objects" + "share both rows and cols"
88
        )
89

90
    if row_match:
8✔
91
        row_names = copy.deepcopy(mats[0].row_names)
8✔
92
        col_names = []
8✔
93
        for mat in mats:
8✔
94
            col_names.extend(copy.deepcopy(mat.col_names))
8✔
95
        x = mats[0].newx.copy()
8✔
96
        for mat in mats[1:]:
8✔
97
            mat.align(mats[0].row_names, axis=0)
8✔
98
            other_x = mat.newx
8✔
99
            x = np.append(x, other_x, axis=1)
8✔
100

101
    else:
102
        col_names = copy.deepcopy(mats[0].col_names)
8✔
103
        row_names = []
8✔
104
        for mat in mats:
8✔
105
            row_names.extend(copy.deepcopy(mat.row_names))
8✔
106
        x = mats[0].newx.copy()
8✔
107
        for mat in mats[1:]:
8✔
108
            mat.align(mats[0].col_names, axis=1)
8✔
109
            other_x = mat.newx
8✔
110
            x = np.append(x, other_x, axis=0)
8✔
111
    return Matrix(x=x, row_names=row_names, col_names=col_names)
8✔
112

113

114
def get_common_elements(list1, list2):
9✔
115
    """find the common elements in two lists.  used to support auto align
116
        might be faster with sets
117

118
    Args:
119
        list1 ([`str`]): a list of strings (could be either row or col
120
            names, depending on calling function)
121
        list2 ([`str`]): a list of strings (could be either row or col
122
            names, depending on calling function)
123

124
    Returns:
125
        [`str`]:  list of common strings shared by list1 and list2
126

127
    Note:
128
        `result` is not ordered WRT `list1` or `list2`
129
    """
130
    set2 = set(list2)
9✔
131
    result = [item for item in list1 if item in set2]
9✔
132
    return result
9✔
133

134

135
class Matrix(object):
9✔
136
    """Easy linear algebra in the PEST(++) realm
9✔
137

138
    Args:
139
        x (`numpy.ndarray`): numeric values
140
        row_names ([`str`]): list of row names
141
        col_names (['str']): list of column names
142
        isdigonal (`bool`): flag if the Matrix is diagonal
143
        autoalign (`bool`): flag to control the autoalignment of Matrix
144
            during linear algebra operations
145

146
    Example::
147

148
        data = np.random.random((10,10))
149
        row_names = ["row_{0}".format(i) for i in range(10)]
150
        col_names = ["col_{0}".format(j) for j in range(10)]
151
        mat = pyemu.Matrix(x=data,row_names=row_names,col_names=col_names)
152
        mat.to_binary("mat.jco")
153

154
        # load an existing jacobian matrix
155
        jco = pyemu.Jco.from_binary("pest.jco")
156
        # form an observation noise covariance matrix from weights
157
        obscov = pyemu.Cov.from_observation_data(pst)
158
        # form the normal matrix, aligning rows and cols on-the-fly
159
        xtqx = jco * obscov.inv * jco.T
160

161

162
    Note:
163
        this class makes heavy use of property decorators to encapsulate
164
        private attributes
165

166
    """
167

168
    integer = np.int32
9✔
169
    double = np.float64
9✔
170
    char = np.uint8
9✔
171

172
    binary_header_dt = np.dtype(
9✔
173
        [("itemp1", integer), ("itemp2", integer), ("icount", integer)]
174
    )
175
    binary_rec_dt = np.dtype([("j", integer), ("dtemp", double)])
9✔
176
    coo_rec_dt = np.dtype([("i", integer), ("j", integer), ("dtemp", double)])
9✔
177

178
    par_length = 12
9✔
179
    obs_length = 20
9✔
180
    new_par_length = 200
9✔
181
    new_obs_length = 200
9✔
182

183
    def __init__(
9✔
184
        self, x=None, row_names=[], col_names=[], isdiagonal=False, autoalign=True
185
    ):
186

187
        self.col_names, self.row_names = [], []
9✔
188
        _ = [self.col_names.append(str(c).lower()) for c in col_names]
9✔
189
        _ = [self.row_names.append(str(r).lower()) for r in row_names]
9✔
190
        self.__x = None
9✔
191
        self.__u = None
9✔
192
        self.__s = None
9✔
193
        self.__v = None
9✔
194
        if x is not None:
9✔
195
            if x.ndim != 2:
9✔
196
                raise Exception("ndim != 2")
×
197
            # x = np.atleast_2d(x)
198
            if isdiagonal and len(row_names) > 0:
9✔
199
                # assert 1 in x.shape,"Matrix error: diagonal matrix must have " +\
200
                #                    "one dimension == 1,shape is {0}".format(x.shape)
201
                mx_dim = max(x.shape)
9✔
202
                if len(row_names) != mx_dim:
9✔
203
                    raise Exception(
×
204
                        "Matrix.__init__(): diagonal shape[1] != len(row_names) "
205
                        + str(x.shape)
206
                        + " "
207
                        + str(len(row_names))
208
                    )
209
                if mx_dim != x.shape[0]:
9✔
210
                    x = x.transpose()
×
211
                # x = x.transpose()
212
            else:
213
                if len(row_names) > 0:
9✔
214
                    if len(row_names) != x.shape[0]:
9✔
215
                        raise Exception(
×
216
                            "Matrix.__init__(): shape[0] != len(row_names) "
217
                            + str(x.shape)
218
                            + " "
219
                            + str(len(row_names))
220
                        )
221

222
                if len(col_names) > 0:
9✔
223
                    # if this a row vector
224
                    if len(row_names) == 0 and x.shape[1] == 1:
9✔
225
                        x.transpose()
×
226
                    if len(col_names) != x.shape[1]:
9✔
227
                        raise Exception(
×
228
                            "Matrix.__init__(): shape[1] != len(col_names) "
229
                            + str(x.shape)
230
                            + " "
231
                            + str(len(col_names))
232
                        )
233
            self.__x = x
9✔
234

235
        self.isdiagonal = bool(isdiagonal)
9✔
236
        self.autoalign = bool(autoalign)
9✔
237

238
    def reset_x(self, x, copy=True):
9✔
239
        """reset self.__x private attribute
240

241
        Args:
242
            x (`numpy.ndarray`): the new numeric data
243
            copy (`bool`): flag to make a copy of 'x'. Default is True
244

245
        Returns:
246
            None
247

248
        Note:
249
            operates in place
250

251
        """
252
        if x.shape != self.shape:
×
253
            raise Exception("shape mismatch")
×
254
        if copy:
×
255
            self.__x = x.copy()
×
256
        else:
257
            self.__x = x
×
258

259
    def __str__(self):
9✔
260
        """overload of object.__str__()
261

262
        Returns:
263
            `str`: string representation
264

265
        """
266
        s = (
9✔
267
            "shape:{0}:{1}".format(*self.shape)
268
            + " row names: "
269
            + str(self.row_names)
270
            + "\n"
271
            + "col names: "
272
            + str(self.col_names)
273
            + "\n"
274
            + str(self.__x)
275
        )
276
        return s
9✔
277

278
    def __getitem__(self, item):
9✔
279
        """a very crude overload of object.__getitem__().
280

281
        Args:
282
            item (`object`): something that can be used as an index
283

284
        Returns:
285
            `Matrix`: an object that is a sub-matrix of `Matrix`
286

287
        """
288
        if self.isdiagonal and isinstance(item, tuple):
9✔
289
            submat = np.atleast_2d((self.__x[item[0]]))
×
290
        else:
291
            submat = np.atleast_2d(self.__x[item])
9✔
292
        # transpose a row vector to a column vector
293
        if submat.shape[0] == 1:
9✔
294
            submat = submat.transpose()
9✔
295
        row_names = self.row_names[: submat.shape[0]]
9✔
296
        if self.isdiagonal:
9✔
297
            col_names = row_names
9✔
298
        else:
299
            col_names = self.col_names[: submat.shape[1]]
9✔
300
        return type(self)(
9✔
301
            x=submat,
302
            isdiagonal=self.isdiagonal,
303
            row_names=row_names,
304
            col_names=col_names,
305
            autoalign=self.autoalign,
306
        )
307

308
    def __pow__(self, power):
9✔
309
        """overload of numpy.ndarray.__pow__() operator
310

311
        Args:
312
            power (`float`): interpreted as follows: -1 = inverse of self,
313
                -0.5 = sqrt of inverse of self,
314
                0.5 = sqrt of self. All other positive
315
                ints = elementwise self raised to power
316

317
        Returns:
318
            `Matrix`: a new Matrix object raised to the power `power`
319

320
        Example::
321

322
            cov = pyemu.Cov.from_uncfile("my.unc")
323
            sqcov = cov**2
324

325
        """
326
        if power < 0:
9✔
327
            if power == -1:
9✔
328
                return self.inv
9✔
329
            elif power == -0.5:
1✔
330
                return (self.inv).sqrt
1✔
331
            else:
332
                raise NotImplementedError(
×
333
                    "Matrix.__pow__() not implemented "
334
                    + "for negative powers except for -1"
335
                )
336

337
        elif int(power) != float(power):
×
338
            if power == 0.5:
×
339
                return self.sqrt
×
340
            else:
341
                raise NotImplementedError(
×
342
                    "Matrix.__pow__() not implemented "
343
                    + "for fractional powers except 0.5"
344
                )
345
        else:
346
            return type(self)(
×
347
                self.__x ** power,
348
                row_names=self.row_names,
349
                col_names=self.col_names,
350
                isdiagonal=self.isdiagonal,
351
            )
352

353
    def __sub__(self, other):
9✔
354
        """numpy.ndarray.__sub__() overload.  Tries to speedup by
355
         checking for scalars of diagonal matrices on either side of operator
356

357
        Args:
358
            other : (`int`,`float`,`numpy.ndarray`,`Matrix`): the thing to subtract
359

360
        Returns:
361
            `Matrix`: the result of subtraction
362

363
        Note:
364
            if `Matrix` and other (if applicable) have `autoalign` set to `True`,
365
            both `Matrix` and `other` are aligned based on row and column names.
366
            If names are not common between the two, this may result in a smaller
367
            returned `Matrix`.  If no names are shared, an exception is raised
368

369
        Example::
370

371
            jco1 = pyemu.Jco.from_binary("pest.1.jcb")
372
            jco2 = pyemu.Jco.from_binary("pest.2.jcb")
373
            diff = jco1 - jco2
374

375

376
        """
377

378
        if np.isscalar(other):
9✔
379
            return Matrix(
×
380
                x=self.x - other,
381
                row_names=self.row_names,
382
                col_names=self.col_names,
383
                isdiagonal=self.isdiagonal,
384
            )
385
        else:
386
            if isinstance(other, pd.DataFrame):
9✔
387
                other = Matrix.from_dataframe(other)
8✔
388

389
            if isinstance(other, np.ndarray):
9✔
390
                if self.shape != other.shape:
8✔
391
                    raise Exception(
×
392
                        "Matrix.__sub__() shape"
393
                        + "mismatch: "
394
                        + str(self.shape)
395
                        + " "
396
                        + str(other.shape)
397
                    )
398
                if self.isdiagonal:
8✔
399
                    elem_sub = -1.0 * other
×
400
                    for j in range(self.shape[0]):
×
401
                        elem_sub[j, j] += self.x[j]
×
402
                    return type(self)(
×
403
                        x=elem_sub, row_names=self.row_names, col_names=self.col_names
404
                    )
405
                else:
406
                    return type(self)(
8✔
407
                        x=self.x - other,
408
                        row_names=self.row_names,
409
                        col_names=self.col_names,
410
                    )
411
            elif isinstance(other, Matrix):
9✔
412
                if (
9✔
413
                    self.autoalign
414
                    and other.autoalign
415
                    and not self.element_isaligned(other)
416
                ):
417
                    common_rows = get_common_elements(self.row_names, other.row_names)
8✔
418
                    common_cols = get_common_elements(self.col_names, other.col_names)
8✔
419

420
                    if len(common_rows) == 0:
8✔
421
                        raise Exception("Matrix.__sub__ error: no common rows")
×
422

423
                    if len(common_cols) == 0:
8✔
424
                        raise Exception("Matrix.__sub__ error: no common cols")
×
425
                    first = self.get(row_names=common_rows, col_names=common_cols)
8✔
426
                    second = other.get(row_names=common_rows, col_names=common_cols)
8✔
427
                else:
428
                    assert self.shape == other.shape, (
9✔
429
                        "Matrix.__sub__():shape mismatch: "
430
                        + str(self.shape)
431
                        + " "
432
                        + str(other.shape)
433
                    )
434
                    first = self
9✔
435
                    second = other
9✔
436

437
                if first.isdiagonal and second.isdiagonal:
9✔
438
                    return type(self)(
8✔
439
                        x=first.x - second.x,
440
                        isdiagonal=True,
441
                        row_names=first.row_names,
442
                        col_names=first.col_names,
443
                    )
444
                elif first.isdiagonal:
9✔
445
                    elem_sub = -1.0 * second.newx
8✔
446
                    for j in range(first.shape[0]):
8✔
447
                        elem_sub[j, j] += first.x[j, 0]
8✔
448
                    return type(self)(
8✔
449
                        x=elem_sub, row_names=first.row_names, col_names=first.col_names
450
                    )
451
                elif second.isdiagonal:
9✔
452
                    elem_sub = first.newx
8✔
453
                    for j in range(second.shape[0]):
8✔
454
                        elem_sub[j, j] -= second.x[j, 0]
8✔
455
                    return type(self)(
8✔
456
                        x=elem_sub, row_names=first.row_names, col_names=first.col_names
457
                    )
458
                else:
459
                    return type(self)(
9✔
460
                        x=first.x - second.x,
461
                        row_names=first.row_names,
462
                        col_names=first.col_names,
463
                    )
464

465
    def __add__(self, other):
9✔
466
        """Overload of numpy.ndarray.__add__() - elementwise addition.  Tries to speedup by checking for
467
            scalars of diagonal matrices on either side of operator
468

469
        Args:
470
            other : (`int`,`float`,`numpy.ndarray`,`Matrix`): the thing to add
471

472
        Returns:
473
            `Matrix`: the result of addition
474

475
        Note:
476
            if `Matrix` and other (if applicable) have `autoalign` set to `True`,
477
            both `Matrix` and `other` are aligned based on row and column names.
478
            If names are not common between the two, this may result in a smaller
479
            returned `Matrix`.  If no names are shared, an exception is raised.
480
            The addition of a scalar does not expand non-zero elements.
481

482
        Example::
483

484
            m1 = pyemu.Cov.from_parameter_data(pst)
485
            m1_plus_on1 = m1 + 1.0 #add 1.0 to all non-zero elements
486
            m2 = m1.copy()
487
            m2_plus_m1 = m1 + m2
488

489

490
        """
491
        if np.isscalar(other):
9✔
492
            return type(self)(
8✔
493
                x=self.x + other,
494
                row_names=self.row_names,
495
                col_names=self.col_names,
496
                isdiagonal=self.isdiagonal,
497
            )
498

499
        if isinstance(other, pd.DataFrame):
9✔
500
            other = Matrix.from_dataframe(other)
8✔
501

502
        if isinstance(other, np.ndarray):
9✔
503
            assert self.shape == other.shape, (
×
504
                "Matrix.__add__(): shape mismatch: "
505
                + str(self.shape)
506
                + " "
507
                + str(other.shape)
508
            )
509
            if self.isdiagonal:
×
510
                raise NotImplementedError(
×
511
                    "Matrix.__add__ not supported for" + "diagonal self"
512
                )
513
            else:
514
                return type(self)(
×
515
                    x=self.x + other, row_names=self.row_names, col_names=self.col_names
516
                )
517

518
        elif isinstance(other, Matrix):
9✔
519
            if self.autoalign and other.autoalign and not self.element_isaligned(other):
9✔
520
                common_rows = get_common_elements(self.row_names, other.row_names)
9✔
521
                common_cols = get_common_elements(self.col_names, other.col_names)
9✔
522
                if len(common_rows) == 0:
9✔
523
                    raise Exception("Matrix.__add__ error: no common rows")
×
524

525
                if len(common_cols) == 0:
9✔
526
                    raise Exception("Matrix.__add__ error: no common cols")
×
527

528
                first = self.get(row_names=common_rows, col_names=common_cols)
9✔
529
                second = other.get(row_names=common_rows, col_names=common_cols)
9✔
530
            else:
531
                assert self.shape == other.shape, (
9✔
532
                    "Matrix.__add__(): shape mismatch: "
533
                    + str(self.shape)
534
                    + " "
535
                    + str(other.shape)
536
                )
537
                first = self
9✔
538
                second = other
9✔
539
            if first.isdiagonal and second.isdiagonal:
9✔
540
                return type(self)(
×
541
                    x=first.x + second.x,
542
                    isdiagonal=True,
543
                    row_names=first.row_names,
544
                    col_names=first.col_names,
545
                )
546
            elif first.isdiagonal:
9✔
547
                ox = second.newx
×
548
                for j in range(first.shape[0]):
×
549
                    ox[j, j] += first.__x[j]
×
550
                return type(self)(
×
551
                    x=ox, row_names=first.row_names, col_names=first.col_names
552
                )
553
            elif second.isdiagonal:
9✔
554
                x = first.x
9✔
555
                js = range(second.shape[0])
9✔
556
                x[js, js] += second.x.ravel()
9✔
557
                # for j in range(second.shape[0]):
558
                #     x[j, j] += second._Matrix__x[j,0]
559
                return type(self)(
9✔
560
                    x=x, row_names=first.row_names, col_names=first.col_names
561
                )
562
            else:
563
                return type(self)(
9✔
564
                    x=first.x + second.x,
565
                    row_names=first.row_names,
566
                    col_names=first.col_names,
567
                )
568
        else:
569
            raise Exception(
×
570
                "Matrix.__add__(): unrecognized type for "
571
                + "other in __add__: "
572
                + str(type(other))
573
            )
574

575
    def hadamard_product(self, other):
9✔
576
        """Overload of numpy.ndarray.__mult__(): element-wise multiplication.
577
        Tries to speedup by checking for scalars of diagonal matrices on
578
        either side of operator
579

580
        Args:
581
            other : (`int`,`float`,`numpy.ndarray`,`Matrix`): the thing to multiply
582

583
        Returns:
584
            `Matrix`: the result of multiplication
585

586
        Note:
587
            if `Matrix` and other (if applicable) have `autoalign` set to `True`,
588
            both `Matrix` and `other` are aligned based on row and column names.
589
            If names are not common between the two, this may result in a smaller
590
            returned `Matrix`.  If not common elements are shared, an excetion is raised
591

592

593
        Example::
594

595
            cov = pyemu.Cov.from_parameter_data(pst)
596
            cov2 = cov * 10
597
            cov3 = cov * cov2
598

599
        """
600
        if np.isscalar(other):
8✔
601
            return type(self)(x=self.x * other)
×
602

603
        if isinstance(other, pd.DataFrame):
8✔
604
            other = Matrix.from_dataframe(other)
8✔
605

606
        if isinstance(other, np.ndarray):
8✔
607
            if other.shape != self.shape:
×
608
                raise Exception(
×
609
                    "Matrix.hadamard_product(): shape mismatch: "
610
                    + str(self.shape)
611
                    + " "
612
                    + str(other.shape)
613
                )
614
            if self.isdiagonal:
×
615
                raise NotImplementedError(
×
616
                    "Matrix.hadamard_product() not supported for" + "diagonal self"
617
                )
618
            else:
619
                return type(self)(
×
620
                    x=self.x * other, row_names=self.row_names, col_names=self.col_names
621
                )
622
        elif isinstance(other, Matrix):
8✔
623
            if self.autoalign and other.autoalign and not self.element_isaligned(other):
8✔
624
                common_rows = get_common_elements(self.row_names, other.row_names)
×
625
                common_cols = get_common_elements(self.col_names, other.col_names)
×
626
                if len(common_rows) == 0:
×
627
                    raise Exception("Matrix.hadamard_product error: no common rows")
×
628

629
                if len(common_cols) == 0:
×
630
                    raise Exception("Matrix.hadamard_product error: no common cols")
×
631

632
                first = self.get(row_names=common_rows, col_names=common_cols)
×
633
                second = other.get(row_names=common_rows, col_names=common_cols)
×
634
            else:
635
                if other.shape != self.shape:
8✔
636
                    raise Exception(
×
637
                        "Matrix.hadamard_product(): shape mismatch: "
638
                        + str(self.shape)
639
                        + " "
640
                        + str(other.shape)
641
                    )
642
                first = self
8✔
643
                second = other
8✔
644

645
            if first.isdiagonal and second.isdiagonal:
8✔
646
                return type(self)(
×
647
                    x=first.x * second.x,
648
                    isdiagonal=True,
649
                    row_names=first.row_names,
650
                    col_names=first.col_names,
651
                )
652
            # elif first.isdiagonal:
653
            #     #ox = second.as_2d
654
            #     #for j in range(first.shape[0]):
655
            #     #    ox[j, j] *= first.__x[j]
656
            #     return type(self)(x=first.as_2d * second.as_2d, row_names=first.row_names,
657
            #                       col_names=first.col_names)
658
            # elif second.isdiagonal:
659
            #     #x = first.as_2d
660
            #     #for j in range(second.shape[0]):
661
            #     #    x[j, j] *= second.x[j]
662
            #     return type(self)(x=first.x * second.as_2d, row_names=first.row_names,
663
            #                       col_names=first.col_names)
664
            else:
665
                return type(self)(
8✔
666
                    x=first.as_2d * second.as_2d,
667
                    row_names=first.row_names,
668
                    col_names=first.col_names,
669
                )
670
        else:
671
            raise Exception(
×
672
                "Matrix.hadamard_product(): unrecognized type for "
673
                + "other: "
674
                + str(type(other))
675
            )
676

677
    def __mul__(self, other):
9✔
678
        """Dot product multiplication overload.  Tries to speedup by
679
        checking for scalars or diagonal matrices on either side of operator
680

681
        Args:
682
            other : (`int`,`float`,`numpy.ndarray`,`Matrix`): the thing to dot product
683

684
        Returns:
685
            `Matrix`: the result of dot product
686

687
        Note:
688
            if `Matrix` and other (if applicable) have `autoalign` set to `True`,
689
            both `Matrix` and `other` are aligned based on row and column names.
690
            If names are not common between the two, this may result in a smaller
691
            returned `Matrix`.  If not common elements are found, an exception is raised
692

693
        Example::
694

695
            jco = pyemu.Jco.from_binary("pest.jcb")
696
            cov = pyemu.Cov.from_parmaeter_data(pst)
697
            # get the forecast prior covariance matrix
698
            forecast_cov = jco.get(pst.forecast_names).T * cov * jco.get(pst.forecast_names)
699

700

701
        """
702

703
        if isinstance(other, pd.DataFrame):
9✔
704
            other = Matrix.from_dataframe(other)
8✔
705

706
        if np.isscalar(other):
9✔
707
            return type(self)(
8✔
708
                x=self.x.copy() * other,
709
                row_names=self.row_names,
710
                col_names=self.col_names,
711
                isdiagonal=self.isdiagonal,
712
            )
713

714
        elif isinstance(other, np.ndarray):
9✔
715

716
            if self.shape[1] != other.shape[0]:
×
717
                raise Exception(
×
718
                    "Matrix.__mul__(): matrices are not aligned: "
719
                    + str(self.shape)
720
                    + " "
721
                    + str(other.shape)
722
                )
723
            if self.isdiagonal:
×
724
                return type(self)(
×
725
                    x=np.dot(np.diag(self.__x.flatten()).transpose(), other)
726
                )
727
            else:
728
                return type(self)(x=np.atleast_2d(np.dot(self.__x, other)))
×
729
        elif isinstance(other, Matrix):
9✔
730
            if self.autoalign and other.autoalign and not self.mult_isaligned(other):
9✔
731
                common = get_common_elements(self.col_names, other.row_names)
9✔
732
                if len(common) == 0:
9✔
733
                    raise Exception(
×
734
                        "Matrix.__mult__():self.col_names "
735
                        + "and other.row_names"
736
                        + "don't share any common elements.  first 10: "
737
                        + ",".join(self.col_names[:9])
738
                        + "...and.."
739
                        + ",".join(other.row_names[:9])
740
                    )
741
                # these should be aligned
742
                if isinstance(self, Cov):
9✔
743
                    first = self.get(row_names=common, col_names=common)
9✔
744
                else:
745
                    first = self.get(row_names=self.row_names, col_names=common)
9✔
746
                if isinstance(other, Cov):
9✔
747
                    second = other.get(row_names=common, col_names=common)
9✔
748
                else:
749
                    second = other.get(row_names=common, col_names=other.col_names)
9✔
750

751
            else:
752
                if self.shape[1] != other.shape[0]:
9✔
753
                    raise Exception(
×
754
                        "Matrix.__mul__(): matrices are not aligned: "
755
                        + str(self.shape)
756
                        + " "
757
                        + str(other.shape)
758
                    )
759
                first = self
9✔
760
                second = other
9✔
761
            if first.isdiagonal and second.isdiagonal:
9✔
762
                elem_prod = type(self)(
×
763
                    x=first.x.transpose() * second.x,
764
                    row_names=first.row_names,
765
                    col_names=second.col_names,
766
                )
767
                elem_prod.isdiagonal = True
×
768
                return elem_prod
×
769
            elif first.isdiagonal:
9✔
770
                ox = second.newx
1✔
771
                for j in range(first.shape[0]):
1✔
772
                    ox[j, :] *= first.x[j]
1✔
773
                return type(self)(
1✔
774
                    x=ox, row_names=first.row_names, col_names=second.col_names
775
                )
776
            elif second.isdiagonal:
9✔
777
                x = first.newx
9✔
778
                ox = second.x
9✔
779
                for j in range(first.shape[1]):
9✔
780
                    x[:, j] *= ox[j]
9✔
781
                return type(self)(
9✔
782
                    x=x, row_names=first.row_names, col_names=second.col_names
783
                )
784
            else:
785
                return type(self)(
9✔
786
                    np.dot(first.x, second.x),
787
                    row_names=first.row_names,
788
                    col_names=second.col_names,
789
                )
790
        else:
791
            raise Exception(
×
792
                "Matrix.__mul__(): unrecognized "
793
                + "other arg type in __mul__: "
794
                + str(type(other))
795
            )
796

797
    def __rmul__(self, other):
9✔
798
        """Reverse order Dot product multiplication overload.
799

800
        Args:
801
            other : (`int`,`float`,`numpy.ndarray`,`Matrix`): the thing to dot product
802

803
        Returns:
804
            `Matrix`: the result of dot product
805

806
        Note:
807
            if `Matrix` and other (if applicable) have `autoalign` set to `True`,
808
            both `Matrix` and `other` are aligned based on row and column names.
809
            If names are not common between the two, this may result in a smaller
810
            returned `Matrix`.  If not common elements are found, an exception is raised
811

812
        Example::
813

814
            # multiply by a scalar
815
            jco = pyemu.Jco.from_binary("pest.jcb")
816
            jco_times_10 = 10 * jco
817

818
        """
819

820
        # if isinstance(other,pd.DataFrame):
821
        #     other = Matrix.from_dataframe(other)
822

823
        if np.isscalar(other):
8✔
824
            return type(self)(
8✔
825
                x=self.x.copy() * other,
826
                row_names=self.row_names,
827
                col_names=self.col_names,
828
                isdiagonal=self.isdiagonal,
829
            )
830

831
        elif isinstance(other, np.ndarray):
×
832
            if self.shape[0] != other.shape[1]:
×
833
                raise Exception(
×
834
                    "Matrix.__rmul__(): matrices are not aligned: "
835
                    + str(other.shape)
836
                    + " "
837
                    + str(self.shape)
838
                )
839
            if self.isdiagonal:
×
840
                return type(self)(
×
841
                    x=np.dot(other, np.diag(self.__x.flatten()).transpose())
842
                )
843
            else:
844
                return type(self)(x=np.dot(other, self.__x))
×
845
        elif isinstance(other, Matrix):
×
846
            if self.autoalign and other.autoalign and not self.mult_isaligned(other):
×
847
                common = get_common_elements(self.row_names, other.col_names)
×
848
                if len(common) == 0:
×
849
                    raise Exception(
×
850
                        "Matrix.__rmul__():self.col_names "
851
                        + "and other.row_names"
852
                        + "don't share any common elements"
853
                    )
854
                # these should be aligned
855
                if isinstance(self, Cov):
×
856
                    first = self.get(row_names=common, col_names=common)
×
857
                else:
858
                    first = self.get(col_names=self.row_names, row_names=common)
×
859
                if isinstance(other, Cov):
×
860
                    second = other.get(row_names=common, col_names=common)
×
861
                else:
862
                    second = other.get(col_names=common, row_names=other.col_names)
×
863

864
            else:
865
                if self.shape[0] != other.shape[1]:
×
866
                    raise Exception(
×
867
                        "Matrix.__rmul__(): matrices are not aligned: "
868
                        + str(other.shape)
869
                        + " "
870
                        + str(self.shape)
871
                    )
872
                first = other
×
873
                second = self
×
874
            if first.isdiagonal and second.isdiagonal:
×
875
                elem_prod = type(self)(
×
876
                    x=first.x.transpose() * second.x,
877
                    row_names=first.row_names,
878
                    col_names=second.col_names,
879
                )
880
                elem_prod.isdiagonal = True
×
881
                return elem_prod
×
882
            elif first.isdiagonal:
×
883
                ox = second.newx
×
884
                for j in range(first.shape[0]):
×
885
                    ox[j, :] *= first.x[j]
×
886
                return type(self)(
×
887
                    x=ox, row_names=first.row_names, col_names=second.col_names
888
                )
889
            elif second.isdiagonal:
×
890
                x = first.newx
×
891
                ox = second.x
×
892
                for j in range(first.shape[1]):
×
893
                    x[:, j] *= ox[j]
×
894
                return type(self)(
×
895
                    x=x, row_names=first.row_names, col_names=second.col_names
896
                )
897
            else:
898
                return type(self)(
×
899
                    np.dot(first.x, second.x),
900
                    row_names=first.row_names,
901
                    col_names=second.col_names,
902
                )
903
        else:
904
            raise Exception(
×
905
                "Matrix.__rmul__(): unrecognized "
906
                + "other arg type in __mul__: "
907
                + str(type(other))
908
            )
909

910
    def __set_svd(self):
9✔
911
        """private method to set SVD components.
912

913
        Note: this should not be called directly
914

915
        """
916
        if self.isdiagonal:
9✔
917
            x = np.diag(self.x.flatten())
×
918
        else:
919
            # just a pointer to x
920
            x = self.x
9✔
921
        try:
9✔
922

923
            u, s, v = np.linalg.svd(x, full_matrices=True)
9✔
924
            v = v.transpose()
9✔
925
        except Exception as e:
×
926
            print("standard SVD failed: {0}".format(str(e)))
×
927
            try:
×
928
                v, s, u = np.linalg.svd(x.transpose(), full_matrices=True)
×
929
                u = u.transpose()
×
930
            except Exception as e:
×
931
                np.savetxt("failed_svd.dat", x, fmt="%15.6E")
×
932
                raise Exception(
×
933
                    "Matrix.__set_svd(): "
934
                    + "unable to compute SVD of self.x, "
935
                    + "saved matrix to 'failed_svd.dat' -- {0}".format(str(e))
936
                )
937

938
        col_names = ["left_sing_vec_" + str(i + 1) for i in range(u.shape[1])]
9✔
939
        self.__u = Matrix(
9✔
940
            x=u, row_names=self.row_names, col_names=col_names, autoalign=False
941
        )
942

943
        sing_names = ["sing_val_" + str(i + 1) for i in range(s.shape[0])]
9✔
944
        self.__s = Matrix(
9✔
945
            x=np.atleast_2d(s).transpose(),
946
            row_names=sing_names,
947
            col_names=sing_names,
948
            isdiagonal=True,
949
            autoalign=False,
950
        )
951

952
        col_names = ["right_sing_vec_" + str(i + 1) for i in range(v.shape[0])]
9✔
953
        self.__v = Matrix(
9✔
954
            v, row_names=self.col_names, col_names=col_names, autoalign=False
955
        )
956

957
    def mult_isaligned(self, other):
9✔
958
        """check if matrices are aligned for dot product multiplication
959

960
        Args:
961
            other (`Matrix`): the other matrix to check for alignment with
962

963
        Returns:
964
            `bool`: True if aligned, False if not aligned
965

966
        """
967
        assert isinstance(
9✔
968
            other, Matrix
969
        ), "Matrix.isaligned(): other argumnent must be type Matrix, not: " + str(
970
            type(other)
971
        )
972
        if self.col_names == other.row_names:
9✔
973
            return True
9✔
974
        else:
975
            return False
9✔
976

977
    def element_isaligned(self, other):
9✔
978
        """check if matrices are aligned for element-wise operations
979

980
        Args:
981
            other (`Matrix`): the other matrix to check for alignment with
982

983
        Returns:
984
            `bool`: True if aligned, False if not aligned
985

986
        """
987
        if not isinstance(other, Matrix):
9✔
988
            raise Exception(
×
989
                "Matrix.isaligned(): other argument must be type Matrix, not: "
990
                + str(type(other))
991
            )
992
        if self.row_names == other.row_names and self.col_names == other.col_names:
9✔
993
            return True
9✔
994
        else:
995
            return False
9✔
996

997
    @property
9✔
998
    def newx(self):
9✔
999
        """return a copy of `Matrix.x` attribute
1000

1001
        Returns:
1002
            `numpy.ndarray`: a copy `Matrix.x`
1003

1004
        """
1005
        return self.__x.copy()
9✔
1006

1007
    @property
9✔
1008
    def x(self):
9✔
1009
        """return a reference to `Matrix.x`
1010

1011
        Returns:
1012
            `numpy.ndarray`: reference to `Matrix.x`
1013

1014
        """
1015
        return self.__x
9✔
1016

1017
    @property
9✔
1018
    def as_2d(self):
9✔
1019
        """get a 2D numeric representation of `Matrix.x`.  If not `isdiagonal`, simply
1020
        return reference to `Matrix.x`, otherwise, constructs and returns
1021
        a 2D, diagonal ndarray
1022

1023
        Returns:
1024
            `numpy.ndarray` : numpy.ndarray
1025

1026
        Exmaple::
1027

1028
            # A diagonal cov
1029
            cov = pyemu.Cov.from_parameter_data
1030
            x2d = cov.as_2d # a numpy 2d array
1031
            print(cov.shape,cov.x.shape,x2d.shape)
1032

1033
        """
1034
        if not self.isdiagonal:
9✔
1035
            return self.x
9✔
1036
        return np.diag(self.x.flatten())
9✔
1037

1038
    def to_2d(self):
9✔
1039
        """get a 2D `Matrix` representation of `Matrix`.  If not `Matrix.isdiagonal`, simply
1040
                return a copy of `Matrix`, otherwise, constructs and returns a new `Matrix`
1041
                instance that is stored as diagonal
1042

1043
        Returns:
1044
            `Martrix`: non-diagonal form of `Matrix`
1045

1046
        Exmaple::
1047

1048
            # A diagonal cov
1049
            cov = pyemu.Cov.from_parameter_data
1050
            cov2d = cov.as_2d # a numpy 2d array
1051
            print(cov.shape,cov.x.shape,cov2d.shape,cov2d.x.shape)
1052

1053
        """
1054
        if not self.isdiagonal:
8✔
1055
            return self.copy()
×
1056
        return type(self)(
8✔
1057
            x=np.diag(self.x.flatten()),
1058
            row_names=self.row_names,
1059
            col_names=self.col_names,
1060
            isdiagonal=False,
1061
        )
1062

1063
    @property
9✔
1064
    def shape(self):
9✔
1065
        """get the implied, 2D shape of `Matrix`
1066

1067
        Returns:
1068
            `int`: length of 2 tuple
1069

1070
        Exmaple::
1071

1072
            jco = pyemu.Jco.from_binary("pest.jcb")
1073
            shape = jco.shape
1074
            nrow,ncol = shape #unpack to ints
1075

1076
        """
1077
        if self.__x is not None:
9✔
1078
            if self.isdiagonal:
9✔
1079
                return (max(self.__x.shape), max(self.__x.shape))
9✔
1080
            if len(self.__x.shape) == 1:
9✔
1081
                raise Exception("Matrix.shape: Matrix objects must be 2D")
×
1082
            return self.__x.shape
9✔
1083
        return None
×
1084

1085
    @property
9✔
1086
    def ncol(self):
9✔
1087
        """length of second dimension
1088

1089
        Returns:
1090
            `int`: number of columns
1091

1092
        """
1093
        return self.shape[1]
9✔
1094

1095
    @property
9✔
1096
    def nrow(self):
9✔
1097
        """length of first dimension
1098

1099
        Returns:
1100
            `int`: number of rows
1101

1102
        """
1103
        return self.shape[0]
9✔
1104

1105
    @property
9✔
1106
    def T(self):
9✔
1107
        """wrapper function for `Matrix.transpose()` method
1108

1109
        Returns:
1110
            `Matrix`: transpose of `Matrix`
1111

1112
        Note:
1113
            returns a copy of self
1114

1115
            A syntatic-sugar overload of Matrix.transpose()
1116

1117
        Example::
1118

1119
            jcb = pyemu.Jco.from_binary("pest.jcb")
1120
            jcbt = jcb.T
1121

1122

1123
        """
1124
        return self.transpose
9✔
1125

1126
    @property
9✔
1127
    def transpose(self):
9✔
1128
        """transpose operation of self
1129

1130
        Returns:
1131
            `Matrix`: transpose of `Matrix`
1132

1133
        Example::
1134

1135
            jcb = pyemu.Jco.from_binary("pest.jcb")
1136
            jcbt = jcb.T
1137

1138
        """
1139
        if not self.isdiagonal:
9✔
1140
            return type(self)(
9✔
1141
                x=self.__x.copy().transpose(),
1142
                row_names=self.col_names,
1143
                col_names=self.row_names,
1144
                autoalign=self.autoalign,
1145
            )
1146
        else:
1147
            return type(self)(
×
1148
                x=self.__x.copy(),
1149
                row_names=self.row_names,
1150
                col_names=self.col_names,
1151
                isdiagonal=True,
1152
                autoalign=self.autoalign,
1153
            )
1154

1155
    @property
9✔
1156
    def inv(self):
9✔
1157
        """inversion operation of `Matrix`
1158

1159
        Returns:
1160
            `Matrix`: inverse of `Matrix`
1161

1162
        Note:
1163
            uses `numpy.linalg.inv` for the inversion
1164

1165
        Example::
1166

1167
            mat = pyemu.Matrix.from_binary("my.jco")
1168
            mat_inv = mat.inv
1169
            mat_inv.to_binary("my_inv.jco")
1170

1171
        """
1172

1173
        if self.isdiagonal:
9✔
1174
            inv = 1.0 / self.__x
9✔
1175
            if np.any(~np.isfinite(inv)):
9✔
1176
                idx = np.isfinite(inv)
×
1177
                np.savetxt("testboo.dat", idx)
×
1178
                invalid = [
×
1179
                    self.row_names[i] for i in range(idx.shape[0]) if idx[i] == 0.0
1180
                ]
1181
                raise Exception(
×
1182
                    "Matrix.inv has produced invalid floating points "
1183
                    + " for the following elements:"
1184
                    + ",".join(invalid)
1185
                )
1186
            return type(self)(
9✔
1187
                x=inv,
1188
                isdiagonal=True,
1189
                row_names=self.row_names,
1190
                col_names=self.col_names,
1191
                autoalign=self.autoalign,
1192
            )
1193
        else:
1194
            return type(self)(
9✔
1195
                x=np.linalg.inv(self.__x),
1196
                row_names=self.row_names,
1197
                col_names=self.col_names,
1198
                autoalign=self.autoalign,
1199
            )
1200

1201
    @staticmethod
9✔
1202
    def get_maxsing_from_s(s, eigthresh=1.0e-5):
9✔
1203
        """static method to work out the maxsing for a
1204
        given singular spectrum
1205

1206
        Args:
1207
            s (`numpy.ndarray`): 1-D array of singular values. This
1208
                array should come from calling either `numpy.linalg.svd`
1209
                or from the `pyemu.Matrix.s.x` attribute
1210
            eigthresh (`float`): the ratio of smallest to largest
1211
                singular value to retain.  Since it is assumed that
1212
                `s` is sorted from largest to smallest, once a singular value
1213
                is reached that yields a ratio with the first (largest)
1214
                singular value, the index of this singular is returned.
1215

1216
        Returns:
1217
            `int`: the index of the singular value whos ratio with the
1218
            first singular value is less than or equal to `eigthresh`
1219

1220

1221
        Example::
1222

1223
            jco = pyemu.Jco.from_binary("pest.jco")
1224
            max_sing = pyemu.Matrix.get_maxsing_from_s(jco.s,eigthresh=pst.svd_data.eigthresh)
1225

1226
        """
1227
        sthresh = s.flatten() / s[0]
8✔
1228
        ising = 0
8✔
1229
        for st in sthresh:
8✔
1230
            if st > eigthresh:
8✔
1231
                ising += 1
8✔
1232
            else:
1233
                break
8✔
1234
        return max(1, ising)
8✔
1235

1236
    def get_maxsing(self, eigthresh=1.0e-5):
9✔
1237
        """Get the number of singular components with a singular
1238
        value ratio greater than or equal to eigthresh
1239

1240
         Args:
1241
            eigthresh (`float`): the ratio of smallest to largest
1242
                singular value to retain.  Since it is assumed that
1243
                `s` is sorted from largest to smallest, once a singular value
1244
                is reached that yields a ratio with the first (largest)
1245
                singular value, the index of this singular is returned.
1246

1247
        Returns:
1248
            `int`: the index of the singular value whos ratio with the
1249
            first singular value is less than or equal to `eigthresh`
1250

1251
        Note:
1252
            this method calls the static method `Matrix.get_maxsing_from_s()`
1253
            with `Matrix.s.x`
1254

1255
        Example::
1256

1257
            jco = pyemu.Jco.from_binary("pest.jco")
1258
            max_sing = jco.get_maxsing(eigthresh=pst.svd_data.eigthresh)
1259

1260
        """
1261

1262
        return Matrix.get_maxsing_from_s(self.s.x, eigthresh=eigthresh)
8✔
1263

1264
    def pseudo_inv_components(self, maxsing=None, eigthresh=1.0e-5, truncate=True):
9✔
1265
        """Get the (optionally) truncated SVD components
1266

1267
        Args:
1268
            maxsing (`int`, optional): the number of singular components to use.  If None,
1269
                `maxsing` is calculated using `Matrix.get_maxsing()` and `eigthresh`
1270
            `eigthresh` : (`float`, optional): the ratio of largest to smallest singular
1271
                components to use for truncation.  Ignored if maxsing is not None.  Default is
1272
                1.0e-5
1273
            truncate (`bool`): flag to truncate components. If False, U, s, and V will be
1274
                zeroed out at locations greater than `maxsing` instead of truncated. Default is True
1275

1276
        Returns:
1277
            tuple containing
1278

1279
            - **Matrix**: (optionally truncated) left singular vectors
1280
            - **Matrix**: (optionally truncated) singular value matrix
1281
            - **Matrix**: (optionally truncated) right singular vectors
1282

1283
        Example::
1284

1285
            mat = pyemu.Matrix.from_binary("my.jco")
1286
            u1,s1,v1 = mat.pseudo_inv_components(maxsing=10)
1287
            resolution_matrix = v1 * v1.T
1288
            resolution_matrix.to_ascii("resol.mat")
1289

1290
        """
1291

1292
        if maxsing is None:
8✔
1293
            maxsing = self.get_maxsing(eigthresh=eigthresh)
×
1294
        else:
1295
            maxsing = min(self.get_maxsing(eigthresh=eigthresh), maxsing)
8✔
1296

1297
        s = self.full_s.copy()
8✔
1298
        v = self.v.copy()
8✔
1299
        u = self.u.copy()
8✔
1300
        if truncate:
8✔
1301

1302
            s = s[:maxsing, :maxsing]
8✔
1303
            v = v[:, :maxsing]
8✔
1304
            u = u[:, :maxsing]
8✔
1305
        else:
1306
            new_s = self.full_s.copy()
8✔
1307
            s = new_s
8✔
1308
            s.x[maxsing:, maxsing:] = 0.0
8✔
1309
            v.x[:, maxsing:] = 0.0
8✔
1310
            u.x[:, maxsing:] = 0.0
8✔
1311

1312
        return u, s, v
8✔
1313

1314
    def pseudo_inv(self, maxsing=None, eigthresh=1.0e-5):
9✔
1315
        """The pseudo inverse of self.  Formed using truncated singular
1316
        value decomposition and `Matrix.pseudo_inv_components`
1317

1318
        Args:
1319
            maxsing (`int`, optional): the number of singular components to use.  If None,
1320
                `maxsing` is calculated using `Matrix.get_maxsing()` and `eigthresh`
1321
            `eigthresh` : (`float`, optional): the ratio of largest to smallest singular
1322
                components to use for truncation.  Ignored if maxsing is not None.  Default is
1323
                1.0e-5
1324

1325
        Returns:
1326
              `Matrix`: the truncated-SVD pseudo inverse of `Matrix` (V_1 * s_1^-1 * U^T)
1327

1328
        Example::
1329

1330
            jco = pyemu.Jco.from_binary("pest.jcb")
1331
            jco_psinv = jco.pseudo_inv(pst.svd_data.maxsing,pst.svd_data.eigthresh)
1332
            jco_psinv.to_binary("pseudo_inv.jcb")
1333
        """
1334
        if maxsing is None:
8✔
1335
            maxsing = self.get_maxsing(eigthresh=eigthresh)
×
1336
        full_s = self.full_s.T
8✔
1337
        for i in range(self.s.shape[0]):
8✔
1338
            if i <= maxsing:
8✔
1339
                full_s.x[i, i] = 1.0 / full_s.x[i, i]
8✔
1340
            else:
1341
                full_s.x[i, i] = 0.0
8✔
1342
        return self.v * full_s * self.u.T
8✔
1343

1344
    @property
9✔
1345
    def sqrt(self):
9✔
1346
        """element-wise square root operation
1347

1348
        Returns:
1349
            `Matrix`: element-wise square root of `Matrix`
1350

1351
        Note:
1352
            uses `numpy.sqrt`
1353

1354
        Example::
1355

1356
            cov = pyemu.Cov.from_uncfile("my.unc")
1357
            sqcov = cov.sqrt
1358
            sqcov.to_uncfile("sqrt_cov.unc")
1359

1360

1361
        """
1362
        if self.isdiagonal:
1✔
1363
            return type(self)(
1✔
1364
                x=np.sqrt(self.__x),
1365
                isdiagonal=True,
1366
                row_names=self.row_names,
1367
                col_names=self.col_names,
1368
                autoalign=self.autoalign,
1369
            )
1370
        elif self.shape[1] == 1:  # a vector
×
1371
            return type(self)(
×
1372
                x=np.sqrt(self.__x),
1373
                isdiagonal=False,
1374
                row_names=self.row_names,
1375
                col_names=self.col_names,
1376
                autoalign=self.autoalign,
1377
            )
1378
        else:
1379
            return type(self)(
×
1380
                x=np.sqrt(self.__x),
1381
                row_names=self.row_names,
1382
                col_names=self.col_names,
1383
                autoalign=self.autoalign,
1384
            )
1385

1386
    @property
9✔
1387
    def full_s(self):
9✔
1388
        """Get the full singular value matrix
1389

1390
        Returns:
1391
            `Matrix`: full singular value matrix.  Shape is `(max(Matrix.shape),max(Matrix.shape))`
1392
            with zeros along the diagonal from `min(Matrix.shape)` to `max(Matrix.shape)`
1393

1394
        Example::
1395

1396
            jco = pyemu.Jco.from_binary("pest.jcb")
1397
            jco.full_s.to_ascii("full_sing_val_matrix.mat")
1398

1399
        """
1400
        x = np.zeros((self.shape), dtype=np.float32)
8✔
1401

1402
        x[: self.s.shape[0], : self.s.shape[0]] = self.s.as_2d
8✔
1403
        s = Matrix(
8✔
1404
            x=x,
1405
            row_names=self.row_names,
1406
            col_names=self.col_names,
1407
            isdiagonal=False,
1408
            autoalign=False,
1409
        )
1410
        return s
8✔
1411

1412
    @property
9✔
1413
    def s(self):
9✔
1414
        """the singular value (diagonal) Matrix
1415

1416
        Returns:
1417
            `Matrix`: singular value matrix.  shape is `(min(Matrix.shape),min(Matrix.shape))`
1418

1419
        Example::
1420

1421
            # plot the singular spectrum of the jacobian
1422
            import matplotlib.pyplot as plt
1423
            jco = pyemu.Jco.from_binary("pest.jcb")
1424
            plt.plot(jco.s.x)
1425
            plt.show()
1426

1427
        """
1428
        if self.__s is None:
9✔
1429
            self.__set_svd()
9✔
1430
        return self.__s
9✔
1431

1432
    @property
9✔
1433
    def u(self):
9✔
1434
        """the left singular vector Matrix
1435

1436
        Returns:
1437
            `Matrix`: left singular vectors.  Shape is `(Matrix.shape[0], Matrix.shape[0])`
1438

1439
        Example::
1440

1441
            jco = pyemu.Jco.from_binary("pest.jcb")
1442
            jco.u.to_binary("u.jcb")
1443

1444

1445
        """
1446
        if self.__u is None:
9✔
1447
            self.__set_svd()
8✔
1448
        return self.__u
9✔
1449

1450
    @property
9✔
1451
    def v(self):
9✔
1452
        """the right singular vector Matrix
1453

1454
        Returns:
1455
            `Matrix`: right singular vectors.  Shape is `(Matrix.shape[1], Matrix.shape[1])`
1456

1457
        Example::
1458

1459
            jco = pyemu.Jco.from_binary("pest.jcb")
1460
            jco.v.to_binary("v.jcb")
1461

1462
        """
1463
        if self.__v is None:
9✔
1464
            self.__set_svd()
9✔
1465
        return self.__v
9✔
1466

1467
    @property
9✔
1468
    def zero2d(self):
9✔
1469
        """get an 2D instance of self with all zeros
1470

1471
        Returns:
1472
            `Matrix`: `Matrix of zeros`
1473

1474
        Example::
1475

1476
            jco = pyemu.Jco.from_binary("pest.jcb")
1477
            zero_jco = jco.zero2d
1478

1479

1480
        """
1481
        return type(self)(
8✔
1482
            x=np.atleast_2d(np.zeros((self.shape[0], self.shape[1]))),
1483
            row_names=self.row_names,
1484
            col_names=self.col_names,
1485
            isdiagonal=False,
1486
        )
1487

1488
    @staticmethod
9✔
1489
    def find_rowcol_indices(names, row_names, col_names, axis=None):
9✔
1490
        """fast(er) look of row and colum names indices
1491

1492
        Args:
1493
            names ([`str`]): list of names to look for in `row_names` and/or `col_names` names
1494
            row_names ([`str`]): list of row names
1495
            col_names([`str`]): list of column names
1496
            axis (`int`, optional): axis to search along.  If None, search both.
1497
                Default is `None`
1498

1499
        Returns:
1500
            `numpy.ndarray`: array of (integer) index locations.  If `axis` is
1501
            `None`, a 2 `numpy.ndarrays` of both row and column name indices is returned
1502

1503
        """
1504

1505
        self_row_idxs = {row_names[i]: i for i in range(len(row_names))}
9✔
1506
        self_col_idxs = {col_names[i]: i for i in range(len(col_names))}
9✔
1507

1508
        scol = set(col_names)
9✔
1509
        srow = set(row_names)
9✔
1510
        row_idxs = []
9✔
1511
        col_idxs = []
9✔
1512
        for name in names:
9✔
1513
            name = name.lower()
9✔
1514
            if name not in scol and name not in srow:
9✔
1515
                raise Exception("Matrix.indices(): name not found: " + name)
×
1516
            if name in scol:
9✔
1517
                col_idxs.append(self_col_idxs[name])
9✔
1518
            if name.lower() in srow:
9✔
1519
                row_idxs.append(self_row_idxs[name])
9✔
1520
        if axis is None:
9✔
1521
            return (
8✔
1522
                np.array(row_idxs, dtype=np.int32),
1523
                np.array(col_idxs, dtype=np.int32),
1524
            )
1525
        elif axis == 0:
9✔
1526
            if len(row_idxs) != len(names):
9✔
1527
                raise Exception(
8✔
1528
                    "Matrix.indices(): " + "not all names found in row_names"
1529
                )
1530
            return np.array(row_idxs, dtype=np.int32)
9✔
1531
        elif axis == 1:
9✔
1532
            if len(col_idxs) != len(names):
9✔
1533
                raise Exception(
×
1534
                    "Matrix.indices(): " + "not all names found in col_names"
1535
                )
1536
            return np.array(col_idxs, dtype=np.int32)
9✔
1537
        else:
1538
            raise Exception(
×
1539
                "Matrix.indices(): " + "axis argument must 0 or 1, not:" + str(axis)
1540
            )
1541

1542
    def indices(self, names, axis=None):
9✔
1543
        """get the row and col indices of names. If axis is None, two ndarrays
1544
                are returned, corresponding the indices of names for each axis
1545

1546
        Args:
1547
            names ([`str`]): list of names to look for in `row_names` and/or `col_names` names
1548
            row_names ([`str`]): list of row names
1549
            col_names([`str`]): list of column names
1550
            axis (`int`, optional): axis to search along.  If None, search both.
1551
                Default is `None`
1552

1553
        Returns:
1554
            `numpy.ndarray`: array of (integer) index locations.  If `axis` is
1555
            `None`, a 2 `numpy.ndarrays` of both row and column name indices is returned
1556

1557
        Note:
1558
            thin wrapper around `Matrix.find_rowcol_indices` static method
1559

1560
        """
1561
        return Matrix.find_rowcol_indices(
9✔
1562
            names, self.row_names, self.col_names, axis=axis
1563
        )
1564

1565
    def align(self, names, axis=None):
9✔
1566
        """reorder `Matrix` by names in place.  If axis is None, reorder both indices
1567

1568
        Args:
1569
            names (['str']): names in `Matrix.row_names` and or `Matrix.col_names`
1570
            axis (`int`, optional): the axis to reorder. if None, reorder both axes
1571

1572
        Note:
1573
              Works in place.
1574
              Is called programmatically during linear algebra operations
1575

1576
        Example::
1577

1578
            # load a jco that has more obs (rows) than non-zero weighted obs
1579
            # in the control file
1580
            jco = pyemu.Jco.from_binary("pest.jcb")
1581
            # get an obs noise cov matrix
1582
            obscov = pyemu.Cov.from_observation_data(pst)
1583
            jco.align(obscov.row_names,axis=0)
1584

1585
        """
1586
        if not isinstance(names, list):
8✔
1587
            names = [names]
×
1588
        row_idxs, col_idxs = self.indices(names)
8✔
1589
        if self.isdiagonal or isinstance(self, Cov):
8✔
1590
            assert row_idxs.shape[0] == self.shape[0]
×
1591
            if row_idxs.shape != col_idxs.shape:
×
1592
                raise Exception("shape mismatch")
×
1593
            if row_idxs.shape[0] != self.shape[0]:
×
1594
                raise Exception("shape mismatch")
×
1595

1596
            if self.isdiagonal:
×
1597
                self.__x = self.__x[row_idxs]
×
1598
            else:
1599
                self.__x = self.__x[row_idxs, :]
×
1600
                self.__x = self.__x[:, col_idxs]
×
1601
            row_names = []
×
1602
            _ = [row_names.append(self.row_names[i]) for i in row_idxs]
×
1603
            self.row_names, self.col_names = row_names, row_names
×
1604

1605
        else:
1606
            if axis is None:
8✔
1607
                raise Exception(
×
1608
                    "Matrix.align(): must specify axis in "
1609
                    + "align call for non-diagonal instances"
1610
                )
1611
            if axis == 0:
8✔
1612
                if row_idxs.shape[0] != self.shape[0]:
8✔
1613
                    raise Exception(
×
1614
                        "Matrix.align(): not all names found in self.row_names"
1615
                    )
1616
                self.__x = self.__x[row_idxs, :]
8✔
1617
                row_names = []
8✔
1618
                _ = [row_names.append(self.row_names[i]) for i in row_idxs]
8✔
1619
                self.row_names = row_names
8✔
1620
            elif axis == 1:
8✔
1621
                if col_idxs.shape[0] != self.shape[1]:
8✔
1622
                    raise Exception(
×
1623
                        "Matrix.align(): not all names found in self.col_names"
1624
                    )
1625
                self.__x = self.__x[:, col_idxs]
8✔
1626
                col_names = []
8✔
1627
                _ = [col_names.append(self.col_names[i]) for i in row_idxs]
8✔
1628
                self.col_names = col_names
8✔
1629
            else:
1630
                raise Exception(
×
1631
                    "Matrix.align(): axis argument to align()"
1632
                    + " must be either 0 or 1"
1633
                )
1634

1635
    def get(self, row_names=None, col_names=None, drop=False):
9✔
1636
        """get a new `Matrix` instance ordered on row_names or col_names
1637

1638
        Args:
1639
            row_names (['str'], optional): row_names for new Matrix.  If `None`,
1640
                all row_names are used.
1641
            col_names (['str'], optional): col_names for new Matrix. If `None`,
1642
                all col_names are used.
1643
            drop (`bool`): flag to remove row_names and/or col_names from this `Matrix`
1644

1645
        Returns:
1646
            `Matrix`: a new `Matrix`
1647

1648
        Example::
1649
            # load a jco that has more obs (rows) than non-zero weighted obs
1650
            # in the control file
1651
            jco = pyemu.Jco.from_binary("pest.jcb")
1652
            # get an obs noise cov matrix
1653
            obscov = pyemu.Cov.from_observation_data(pst)
1654
            nnz_jco = jco.get(row_names = obscov.row_names)
1655

1656

1657
        """
1658
        if row_names is None and col_names is None:
9✔
1659
            raise Exception(
×
1660
                "Matrix.get(): must pass at least" + " row_names or col_names"
1661
            )
1662

1663
        if row_names is not None and not isinstance(row_names, list):
9✔
1664
            row_names = [row_names]
1✔
1665
        if col_names is not None and not isinstance(col_names, list):
9✔
1666
            col_names = [col_names]
9✔
1667

1668
        if isinstance(self, Cov) and (row_names is None or col_names is None):
9✔
1669
            if row_names is not None:
9✔
1670
                idxs = self.indices(row_names, axis=0)
9✔
1671
                names = row_names
9✔
1672
            else:
1673
                idxs = self.indices(col_names, axis=1)
9✔
1674
                names = col_names
9✔
1675

1676
            if self.isdiagonal:
9✔
1677
                extract = self.__x[idxs].copy()
9✔
1678
            else:
1679
                extract = self.__x[idxs, :].copy()
9✔
1680
                extract = extract[:, idxs]
9✔
1681
            if drop:
9✔
1682
                self.drop(names, 0)
9✔
1683
            return Cov(x=extract, names=names, isdiagonal=self.isdiagonal)
9✔
1684
        if self.isdiagonal:
9✔
1685
            extract = np.diag(self.__x[:, 0])
9✔
1686
        else:
1687
            extract = self.__x.copy()
9✔
1688
        if row_names is not None:
9✔
1689
            row_idxs = self.indices(row_names, axis=0)
9✔
1690
            extract = np.atleast_2d(extract[row_idxs, :].copy())
9✔
1691
            if drop:
9✔
1692
                self.drop(row_names, axis=0)
9✔
1693
        else:
1694
            row_names = self.row_names
9✔
1695
        if col_names is not None:
9✔
1696
            col_idxs = self.indices(col_names, axis=1)
9✔
1697
            extract = np.atleast_2d(extract[:, col_idxs].copy())
9✔
1698
            if drop:
9✔
1699
                self.drop(col_names, axis=1)
9✔
1700
        else:
1701
            col_names = copy.deepcopy(self.col_names)
9✔
1702

1703
        return type(self)(x=extract, row_names=row_names, col_names=col_names)
9✔
1704

1705
    def copy(self):
9✔
1706
        """get a copy of `Matrix`
1707

1708
        Returns:
1709
            `Matrix`: copy of this `Matrix`
1710

1711
        """
1712
        return type(self)(
9✔
1713
            x=self.newx,
1714
            row_names=self.row_names,
1715
            col_names=self.col_names,
1716
            isdiagonal=self.isdiagonal,
1717
            autoalign=self.autoalign,
1718
        )
1719

1720
    def drop(self, names, axis):
9✔
1721
        """drop elements from `Matrix` in place
1722

1723
        Args:
1724
            names (['str']): list of names to drop
1725
            axis (`int`): the axis to drop from. must be in [0,1]
1726

1727
        """
1728
        if axis is None:
9✔
1729
            raise Exception("Matrix.drop(): axis arg is required")
×
1730
        if not isinstance(names, list):
9✔
1731
            names = [names]
8✔
1732
        if axis == 1:
9✔
1733
            if len(names) >= self.shape[1]:
9✔
1734
                raise Exception("can't drop all names along axis 1")
×
1735
        else:
1736
            if len(names) >= self.shape[0]:
9✔
1737
                raise Exception("can't drop all names along axis 0")
×
1738

1739
        idxs = self.indices(names, axis=axis)
9✔
1740

1741
        if self.isdiagonal:
9✔
1742
            self.__x = np.delete(self.__x, idxs, 0)
9✔
1743
            keep_names = [name for name in self.row_names if name not in names]
9✔
1744
            if len(keep_names) != self.__x.shape[0]:
9✔
1745
                raise Exception(
×
1746
                    "shape-name mismatch:"
1747
                    + "{0}:{0}".format(len(keep_names), self.__x.shape)
1748
                )
1749
            self.row_names = keep_names
9✔
1750
            self.col_names = copy.deepcopy(keep_names)
9✔
1751
            # idxs = np.sort(idxs)
1752
            # for idx in idxs[::-1]:
1753
            #     del self.row_names[idx]
1754
            #     del self.col_names[idx]
1755
        elif isinstance(self, Cov):
9✔
1756
            self.__x = np.delete(self.__x, idxs, 0)
8✔
1757
            self.__x = np.delete(self.__x, idxs, 1)
8✔
1758
            keep_names = [name for name in self.row_names if name not in names]
8✔
1759

1760
            if len(keep_names) != self.__x.shape[0]:
8✔
1761
                raise Exception(
×
1762
                    "shape-name mismatch:"
1763
                    + "{0}:{0}".format(len(keep_names), self.__x.shape)
1764
                )
1765
            self.row_names = keep_names
8✔
1766
            self.col_names = copy.deepcopy(keep_names)
8✔
1767
            # idxs = np.sort(idxs)
1768
            # for idx in idxs[::-1]:
1769
            #     del self.row_names[idx]
1770
            #     del self.col_names[idx]
1771
        elif axis == 0:
9✔
1772
            if idxs.shape[0] == self.shape[0]:
9✔
1773
                raise Exception("Matrix.drop(): can't drop all rows")
×
1774
            elif idxs.shape == 0:
9✔
1775
                raise Exception("Matrix.drop(): nothing to drop on axis 0")
×
1776
            self.__x = np.delete(self.__x, idxs, 0)
9✔
1777
            keep_names = [name for name in self.row_names if name not in names]
9✔
1778
            if len(keep_names) != self.__x.shape[0]:
9✔
1779
                raise Exception(
×
1780
                    "shape-name mismatch:"
1781
                    + "{0}:{1}".format(len(keep_names), self.__x.shape)
1782
                )
1783
            self.row_names = keep_names
9✔
1784
            # idxs = np.sort(idxs)
1785
            # for idx in idxs[::-1]:
1786
            #     del self.row_names[idx]
1787
        elif axis == 1:
9✔
1788
            if idxs.shape[0] == self.shape[1]:
9✔
1789
                raise Exception("Matrix.drop(): can't drop all cols")
×
1790
            if idxs.shape == 0:
9✔
1791
                raise Exception("Matrix.drop(): nothing to drop on axis 1")
×
1792
            self.__x = np.delete(self.__x, idxs, 1)
9✔
1793
            keep_names = [name for name in self.col_names if name not in names]
9✔
1794
            if len(keep_names) != self.__x.shape[1]:
9✔
1795
                raise Exception(
×
1796
                    "shape-name mismatch:"
1797
                    + "{0}:{1}".format(len(keep_names), self.__x.shape)
1798
                )
1799
            self.col_names = keep_names
9✔
1800
            # idxs = np.sort(idxs)
1801
            # for idx in idxs[::-1]:
1802
            #     del self.col_names[idx]
1803
        else:
1804
            raise Exception("Matrix.drop(): axis argument must be 0 or 1")
×
1805

1806
    def extract(self, row_names=None, col_names=None):
9✔
1807
        """wrapper method that `Matrix.gets()` then `Matrix.drops()` elements.
1808
        one of row_names or col_names must be not None.
1809

1810
        Args:
1811
            row_names (['str'], optional): row_names to extract.  If `None`,
1812
                all row_names are retained.
1813
            col_names (['str'], optional): col_names to extract. If `None`,
1814
                all col_names are retained.
1815

1816
        Returns:
1817
            `Matrix`: the extract sub-matrix defined by `row_names` and/or `col_names`
1818

1819
        Example::
1820

1821
            cov = pyemu.Cov.from_parameter_data(pst)
1822
            hk_cov = cov.extract(row_names=["hk1","hk2","hk3"])
1823

1824
        """
1825
        if row_names is None and col_names is None:
9✔
1826
            raise Exception("Matrix.extract() " + "row_names and col_names both None")
×
1827
        extract = self.get(row_names, col_names, drop=True)
9✔
1828
        return extract
9✔
1829

1830
    def get_diagonal_vector(self, col_name="diag"):
9✔
1831
        """Get a new Matrix instance that is the diagonal of self.  The
1832
        shape of the new matrix is (self.shape[0],1).  Self must be square
1833

1834
        Args:
1835
            col_name (`str`): the name of the single column in the new Matrix
1836

1837
        Returns:
1838
            `Matrix`: vector-shaped `Matrix` instance of the diagonal of this `Matrix`
1839

1840
        Example::
1841

1842
            cov = pyemu.Cov.from_unc_file("my.unc")
1843
            cov_diag = cov.get_diagonal_vector()
1844
            print(cov_diag.col_names)
1845

1846
        """
1847
        if self.shape[0] != self.shape[1]:
9✔
1848
            raise Exception("not diagonal")
×
1849
        if self.isdiagonal:
9✔
1850
            raise Exception("already diagonal")
×
1851
        if not isinstance(col_name, str):
9✔
1852
            raise Exception("col_name must be type str")
×
1853
        return type(self)(
9✔
1854
            x=np.atleast_2d(np.diag(self.x)).transpose(),
1855
            row_names=self.row_names,
1856
            col_names=[col_name],
1857
            isdiagonal=False,
1858
        )
1859

1860
    def to_coo(self, filename, droptol=None, chunk=None):
9✔
1861
        """write an extended PEST-format binary file.  The data format is
1862
        [int,int,float] for i,j,value.  It is autodetected during
1863
        the read with `Matrix.from_binary()`.
1864

1865
        Args:
1866
            filename (`str`): filename to save binary file
1867
            droptol (`float`): absolute value tolerance to make values
1868
                smaller `droptol` than zero.  Default is None (no dropping)
1869
            chunk (`int`): number of elements to write in a single pass.
1870
                Default is `None`, which writes the entire numeric part of the
1871
                `Matrix` at once. This is faster but requires more memory.
1872

1873
        Note:
1874
            This method is needed when the number of dimensions times 2 is larger
1875
            than the max value for a 32-bit integer.  happens!
1876
            This method is used by pyemu.Ensemble.to_binary()
1877

1878
        """
1879
        if self.isdiagonal:
9✔
1880
            # raise NotImplementedError()
1881
            self.__x = self.as_2d
×
1882
            self.isdiagonal = False
×
1883
        if droptol is not None:
9✔
1884
            self.x[np.abs(self.x) < droptol] = 0.0
×
1885
        f = open(filename, "wb")
9✔
1886
        # print("counting nnz")
1887
        nnz = np.count_nonzero(self.x)  # number of non-zero entries
9✔
1888
        # write the header
1889
        header = np.array(
9✔
1890
            (self.shape[1], self.shape[0], nnz), dtype=self.binary_header_dt
1891
        )
1892
        header.tofile(f)
9✔
1893
        # get the indices of non-zero entries
1894
        # print("getting nnz idxs")
1895
        row_idxs, col_idxs = np.nonzero(self.x)
9✔
1896

1897
        if chunk is None:
9✔
1898
            flat = self.x[row_idxs, col_idxs].flatten()
9✔
1899
            data = np.core.records.fromarrays(
9✔
1900
                [row_idxs, col_idxs, flat], dtype=self.coo_rec_dt
1901
            )
1902
            data.tofile(f)
9✔
1903
        else:
1904

1905
            start, end = 0, min(chunk, row_idxs.shape[0])
8✔
1906
            while True:
4✔
1907
                # print(row_idxs[start],row_idxs[end])
1908
                # print("chunk",start,end)
1909
                flat = self.x[row_idxs[start:end], col_idxs[start:end]].flatten()
8✔
1910
                data = np.core.records.fromarrays(
8✔
1911
                    [row_idxs[start:end], col_idxs[start:end], flat],
1912
                    dtype=self.coo_rec_dt,
1913
                )
1914
                data.tofile(f)
8✔
1915
                if end == row_idxs.shape[0]:
8✔
1916
                    break
8✔
1917
                start = end
8✔
1918
                end = min(row_idxs.shape[0], start + chunk)
8✔
1919

1920
        for name in self.col_names:
9✔
1921
            if len(name) > self.new_par_length:
9✔
1922
                warnings.warn(
×
1923
                    "par name '{0}' greater than {1} chars".format(
1924
                        name, self.new_par_length
1925
                    )
1926
                )
1927
                name = name[: self.new_par_length - 1]
×
1928
            elif len(name) < self.new_par_length:
9✔
1929
                for _ in range(len(name), self.new_par_length):
9✔
1930
                    name = name + " "
9✔
1931
            f.write(name.encode())
9✔
1932
        for name in self.row_names:
9✔
1933
            if len(name) > self.new_obs_length:
9✔
1934
                warnings.warn(
×
1935
                    "obs name '{0}' greater than {1} chars".format(
1936
                        name, self.new_obs_length
1937
                    )
1938
                )
1939
                name = name[: self.new_obs_length - 1]
×
1940
            elif len(name) < self.new_obs_length:
9✔
1941
                for _ in range(len(name), self.new_obs_length):
9✔
1942
                    name = name + " "
9✔
1943
            f.write(name.encode())
9✔
1944
        f.close()
9✔
1945

1946
    def to_dense(self, filename, close=True):
9✔
1947
        """experimental new dense matrix storage format to support faster I/O with ensembles
1948

1949
        Args:
1950
            filename (`str`): the filename to save to
1951
            close (`bool`): flag to close the filehandle after saving
1952

1953
        Returns:
1954
            f (`file`): the file handle.  Only returned if `close` is False
1955

1956
        Note:
1957
            calls Matrix.write_dense()
1958

1959

1960
        """
1961
        return Matrix.write_dense(
8✔
1962
            filename,
1963
            row_names=self.row_names,
1964
            col_names=self.col_names,
1965
            data=self.x,
1966
            close=close,
1967
        )
1968

1969
    @staticmethod
9✔
1970
    def write_dense(filename, row_names, col_names, data, close=False):
9✔
1971
        """experimental new dense matrix storage format to support faster I/O with ensembles
1972

1973
        Args:
1974
            filename (`str` or `file`): the file to write to
1975
            row_names ([`str`]): row names of the matrix
1976
            col_names ([`str`]): col names of the matrix
1977
            data (`np.ndarray`): matrix elements
1978
            close (`bool`): flag to close the file after writing
1979

1980
        Returns:
1981
            f (`file`): the file handle.  Only returned if `close` is False
1982

1983
        """
1984

1985
        if isinstance(filename, str):
8✔
1986
            f = open(filename, "wb")
8✔
1987
            header = np.array(
8✔
1988
                (0, -len(col_names), -len(col_names)), dtype=Matrix.binary_header_dt
1989
            )
1990
            header.tofile(f)
8✔
1991
            slengths = np.array(
8✔
1992
                [len(col_name) for col_name in col_names], dtype=Matrix.integer
1993
            )
1994
            slengths.tofile(f)
8✔
1995
            for i, col_name in enumerate(col_names):
8✔
1996
                # slengths[[i]].tofile(f)
1997
                f.write(col_name.encode())
8✔
1998
        else:
1999
            f = filename
8✔
2000
        slengths = np.array(
8✔
2001
            [len(row_name) for row_name in row_names], dtype=Matrix.integer
2002
        )
2003
        for i in range(data.shape[0]):
8✔
2004
            slengths[[i]].tofile(f)
8✔
2005
            f.write(row_names[i].encode())
8✔
2006
            data[i, :].astype(Matrix.double).tofile(f)
8✔
2007
        if close:
8✔
2008
            f.close()
8✔
2009
        else:
2010
            return f
8✔
2011

2012
    def to_binary(self, filename, droptol=None, chunk=None):
9✔
2013
        """write a PEST-compatible binary file.  The format is the same
2014
        as the format used to storage a PEST Jacobian matrix
2015

2016
        Args:
2017
            filename (`str`): filename to save binary file
2018
            droptol (`float`): absolute value tolerance to make values
2019
                smaller `droptol` than zero.  Default is None (no dropping)
2020
            chunk (`int`): number of elements to write in a single pass.
2021
                Default is `None`, which writes the entire numeric part of the
2022
                `Matrix` at once. This is faster but requires more memory.
2023

2024
        """
2025
        # print(self.x)
2026
        # print(type(self.x))
2027

2028
        if np.any(np.isnan(self.x)):
9✔
2029
            raise Exception("Matrix.to_binary(): nans found")
×
2030
        if self.isdiagonal:
9✔
2031
            # raise NotImplementedError()
2032
            self.__x = self.as_2d
8✔
2033
            self.isdiagonal = False
8✔
2034
        if droptol is not None:
9✔
2035
            self.x[np.abs(self.x) < droptol] = 0.0
×
2036
        f = open(filename, "wb")
9✔
2037
        nnz = np.count_nonzero(self.x)  # number of non-zero entries
9✔
2038
        # write the header
2039
        header = np.array(
9✔
2040
            (-self.shape[1], -self.shape[0], nnz), dtype=self.binary_header_dt
2041
        )
2042
        header.tofile(f)
9✔
2043
        # get the indices of non-zero entries
2044
        row_idxs, col_idxs = np.nonzero(self.x)
9✔
2045
        icount = row_idxs + 1 + col_idxs * self.shape[0]
9✔
2046
        # flatten the array
2047
        # flat = self.x[row_idxs, col_idxs].flatten()
2048
        # zip up the index position and value pairs
2049
        # data = np.array(list(zip(icount, flat)), dtype=self.binary_rec_dt)
2050

2051
        if chunk is None:
9✔
2052
            flat = self.x[row_idxs, col_idxs].flatten()
9✔
2053
            data = np.core.records.fromarrays([icount, flat], dtype=self.binary_rec_dt)
9✔
2054
            # write
2055
            data.tofile(f)
9✔
2056
        else:
2057
            start, end = 0, min(chunk, row_idxs.shape[0])
×
2058
            while True:
2059
                # print(row_idxs[start],row_idxs[end])
2060
                flat = self.x[row_idxs[start:end], col_idxs[start:end]].flatten()
×
2061
                data = np.core.records.fromarrays(
×
2062
                    [icount[start:end], flat], dtype=self.binary_rec_dt
2063
                )
2064
                data.tofile(f)
×
2065
                if end == row_idxs.shape[0]:
×
2066
                    break
×
2067
                start = end
×
2068
                end = min(row_idxs.shape[0], start + chunk)
×
2069

2070
        for name in self.col_names:
9✔
2071
            if len(name) > self.par_length:
9✔
2072
                warnings.warn(
8✔
2073
                    "par name '{0}' greater than {1} chars".format(
2074
                        name, self.par_length
2075
                    )
2076
                )
2077
                name = name[: self.par_length]
8✔
2078
            elif len(name) < self.par_length:
9✔
2079
                for i in range(len(name), self.par_length):
9✔
2080
                    name = name + " "
9✔
2081
            f.write(name.encode())
9✔
2082
        for name in self.row_names:
9✔
2083
            if len(name) > self.obs_length:
9✔
2084
                warnings.warn(
8✔
2085
                    "obs name '{0}' greater than {1} chars".format(
2086
                        name, self.obs_length
2087
                    )
2088
                )
2089
                name = name[: self.obs_length]
8✔
2090
            elif len(name) < self.obs_length:
9✔
2091
                for i in range(len(name), self.obs_length):
9✔
2092
                    name = name + " "
9✔
2093
            f.write(name.encode())
9✔
2094

2095
        f.close()
9✔
2096

2097
    @staticmethod
9✔
2098
    def read_dense(filename, forgive=False, close=True):
9✔
2099
        """read a dense-format binary file.
2100

2101
        Args:
2102
            filename (`str`): the filename or the open filehandle
2103
            forgive (`bool`): flag to forgive incomplete records.  If True and
2104
                an incomplete record is encountered, only the previously read
2105
                records are returned.  If False, an exception is raised for an
2106
                incomplete record
2107
            close (`bool`): flag to close the filehandle.  Default is True
2108

2109
        Returns:
2110
            tuple containing
2111

2112
            - **numpy.ndarray**: the numeric values in the file
2113
            - **['str']**: list of row names
2114
            - **[`str`]**: list of col_names
2115

2116

2117
        """
2118
        if not os.path.exists(filename):
8✔
2119
            raise Exception(
×
2120
                "Matrix.read_dense(): filename '{0}' not found".format(filename)
2121
            )
2122
        if isinstance(filename, str):
8✔
2123
            f = open(filename, "rb")
8✔
2124
        else:
2125
            f = filename
×
2126
        # the header datatype
2127
        itemp1, itemp2, icount = np.fromfile(f, Matrix.binary_header_dt, 1)[0]
8✔
2128
        # print(itemp1,itemp2,icount)
2129
        if itemp1 != 0:
8✔
2130
            raise Exception("Matrix.read_dense() itemp1 != 0")
×
2131
        if itemp2 != icount:
8✔
2132
            raise Exception("Matrix.read_dense() itemp2 != icount")
×
2133
        ncol = np.abs(itemp2)
8✔
2134
        col_names = []
8✔
2135
        row_names = []
8✔
2136
        data_rows = []
8✔
2137
        col_slens = np.fromfile(f, Matrix.integer, ncol)
8✔
2138
        i = 0
8✔
2139
        # for j in range(ncol):
2140
        for slen in col_slens:
8✔
2141
            # slen = np.fromfile(f, Matrix.integer,1)[0]
2142
            name = (
8✔
2143
                struct.unpack(str(slen) + "s", f.read(slen))[0].strip().lower().decode()
2144
            )
2145
            col_names.append(name)
8✔
2146
        while True:
4✔
2147
            try:
8✔
2148
                slen = np.fromfile(f, Matrix.integer, 1)[0]
8✔
2149
            except Exception as e:
8✔
2150
                break
8✔
2151
            try:
8✔
2152
                name = (
8✔
2153
                    struct.unpack(str(slen) + "s", f.read(slen))[0]
2154
                    .strip()
2155
                    .lower()
2156
                    .decode()
2157
                )
2158

2159
                data_row = np.fromfile(f, Matrix.double, ncol)
8✔
2160
                if data_row.shape[0] != ncol:
8✔
2161
                    raise Exception(
8✔
2162
                        "incomplete data in row {0}: {1} vs {2}".format(
2163
                            i, data_row.shape[0], ncol
2164
                        )
2165
                    )
2166
            except Exception as e:
8✔
2167
                if forgive:
8✔
2168
                    print("error reading row {0}: {1}".format(i, str(e)))
8✔
2169
                    break
8✔
2170
                else:
2171
                    raise Exception("error reading row {0}: {1}".format(i, str(e)))
8✔
2172
            row_names.append(name)
8✔
2173
            data_rows.append(data_row)
8✔
2174
            i += 1
8✔
2175

2176
        data_rows = np.array(data_rows)
8✔
2177
        if close:
8✔
2178
            f.close()
8✔
2179
        return data_rows, row_names, col_names
8✔
2180

2181
    @classmethod
9✔
2182
    def from_binary(cls, filename, forgive=False):
9✔
2183
        """class method load from PEST-compatible binary file into a
2184
        Matrix instance
2185

2186
        Args:
2187
            filename (`str`): filename to read
2188
            forgive (`bool`): flag to forgive incomplete data records. Only
2189
                applicable to dense binary format.  Default is `False`
2190

2191
        Returns:
2192
            `Matrix`: `Matrix` loaded from binary file
2193

2194
        Example::
2195

2196
            mat = pyemu.Matrix.from_binary("my.jco")
2197
            cov = pyemu.Cov.from_binary("large_cov.jcb")
2198

2199
        """
2200
        x, row_names, col_names = Matrix.read_binary(filename, forgive=forgive)
9✔
2201
        if np.any(np.isnan(x)):
9✔
2202
            warnings.warn("Matrix.from_binary(): nans in matrix", PyemuWarning)
×
2203
        return cls(x=x, row_names=row_names, col_names=col_names)
9✔
2204

2205
    @staticmethod
9✔
2206
    def read_binary_header(filename):
9✔
2207
        """read the first elements of a PEST(++)-style binary file to get
2208
        format and dimensioning information.
2209

2210
        Args:
2211
            filename (`str`): the filename of the binary file
2212

2213
        Returns:
2214
            tuple containing
2215

2216
            - **int**: the itemp1 value
2217
            - **int**: the itemp2 value
2218
            - **int**: the icount value
2219

2220
        """
2221
        if not os.path.exists(filename):
×
2222
            raise Exception(
×
2223
                "Matrix.read_binary_header(): filename '{0}' not found".format(filename)
2224
            )
2225
        f = open(filename, "rb")
×
2226
        itemp1, itemp2, icount = np.fromfile(f, Matrix.binary_header_dt, 1)[0]
×
2227
        f.close()
×
2228
        return itemp1, itemp2, icount
×
2229

2230
    @staticmethod
9✔
2231
    def read_binary(filename, forgive=False):
9✔
2232
        """static method to read PEST-format binary files
2233

2234
        Args:
2235
            filename (`str`): filename to read
2236
            forgive (`bool`): flag to forgive incomplete data records. Only
2237
                applicable to dense binary format.  Default is `False`
2238

2239

2240
        Returns:
2241
            tuple containing
2242

2243
            - **numpy.ndarray**: the numeric values in the file
2244
            - **['str']**: list of row names
2245
            - **[`str`]**: list of col_names
2246

2247
        """
2248
        if not os.path.exists(filename):
9✔
2249
            raise Exception(
×
2250
                "Matrix.read_binary(): filename '{0}' not found".format(filename)
2251
            )
2252
        f = open(filename, "rb")
9✔
2253
        itemp1, itemp2, icount = np.fromfile(f, Matrix.binary_header_dt, 1)[0]
9✔
2254
        if itemp1 > 0 and itemp2 < 0 and icount < 0:
9✔
2255
            print(
×
2256
                " WARNING: it appears this file was \n"
2257
                + " written with 'sequential` "
2258
                + " binary fortran specification\n...calling "
2259
                + " Matrix.from_fortranfile()"
2260
            )
2261
            f.close()
×
2262
            return Matrix.from_fortranfile(filename)
×
2263
        if itemp1 == 0 and itemp2 == icount:
9✔
2264
            f.close()
8✔
2265
            return Matrix.read_dense(filename, forgive=forgive)
8✔
2266

2267
        ncol, nrow = abs(itemp1), abs(itemp2)
9✔
2268
        if itemp1 >= 0:
9✔
2269
            # raise TypeError('Matrix.from_binary(): Jco produced by ' +
2270
            #                 'deprecated version of PEST,' +
2271
            #                 'Use JcoTRANS to convert to new format')
2272
            # print("new binary format detected...")
2273

2274
            data = np.fromfile(f, Matrix.coo_rec_dt, icount)
9✔
2275
            if data["i"].min() < 0:
9✔
2276
                raise Exception("Matrix.from_binary(): 'i' index values less than 0")
×
2277
            if data["j"].min() < 0:
9✔
2278
                raise Exception("Matrix.from_binary(): 'j' index values less than 0")
×
2279
            x = np.zeros((nrow, ncol))
9✔
2280
            x[data["i"], data["j"]] = data["dtemp"]
9✔
2281
            data = x
9✔
2282
            # read obs and parameter names
2283
            col_names = []
9✔
2284
            row_names = []
9✔
2285
            for j in range(ncol):
9✔
2286
                name = (
9✔
2287
                    struct.unpack(
2288
                        str(Matrix.new_par_length) + "s", f.read(Matrix.new_par_length)
2289
                    )[0]
2290
                    .strip()
2291
                    .lower()
2292
                    .decode()
2293
                )
2294
                col_names.append(name)
9✔
2295
            for i in range(nrow):
9✔
2296
                name = (
9✔
2297
                    struct.unpack(
2298
                        str(Matrix.new_obs_length) + "s", f.read(Matrix.new_obs_length)
2299
                    )[0]
2300
                    .strip()
2301
                    .lower()
2302
                    .decode()
2303
                )
2304
                row_names.append(name)
9✔
2305
            f.close()
9✔
2306
        else:
2307

2308
            # read all data records
2309
            # using this a memory hog, but really fast
2310
            data = np.fromfile(f, Matrix.binary_rec_dt, icount)
9✔
2311
            icols = ((data["j"] - 1) // nrow) + 1
9✔
2312
            irows = data["j"] - ((icols - 1) * nrow)
9✔
2313
            x = np.zeros((nrow, ncol))
9✔
2314
            x[irows - 1, icols - 1] = data["dtemp"]
9✔
2315
            data = x
9✔
2316
            # read obs and parameter names
2317
            col_names = []
9✔
2318
            row_names = []
9✔
2319
            for j in range(ncol):
9✔
2320
                name = (
9✔
2321
                    struct.unpack(
2322
                        str(Matrix.par_length) + "s", f.read(Matrix.par_length)
2323
                    )[0]
2324
                    .strip()
2325
                    .lower()
2326
                    .decode()
2327
                )
2328
                col_names.append(name)
9✔
2329
            for i in range(nrow):
9✔
2330

2331
                name = (
9✔
2332
                    struct.unpack(
2333
                        str(Matrix.obs_length) + "s", f.read(Matrix.obs_length)
2334
                    )[0]
2335
                    .strip()
2336
                    .lower()
2337
                    .decode()
2338
                )
2339
                row_names.append(name)
9✔
2340
            f.close()
9✔
2341
        if len(row_names) != data.shape[0]:
9✔
2342
            raise Exception(
×
2343
                "Matrix.read_binary() len(row_names) ("
2344
                + str(len(row_names))
2345
                + ") != x.shape[0] ("
2346
                + str(data.shape[0])
2347
                + ")"
2348
            )
2349
        if len(col_names) != data.shape[1]:
9✔
2350
            raise Exception(
×
2351
                "Matrix.read_binary() len(col_names) ("
2352
                + str(len(col_names))
2353
                + ") != self.shape[1] ("
2354
                + str(data.shape[1])
2355
                + ")"
2356
            )
2357
        return data, row_names, col_names
9✔
2358

2359
    @staticmethod
9✔
2360
    def from_fortranfile(filename):
9✔
2361
        """a binary load method to accommodate one of the many
2362
            bizarre fortran binary writing formats
2363

2364
        Args:
2365
            filename (`str`): name of the binary matrix file
2366

2367
        Returns:
2368
            tuple containing
2369

2370
            - **numpy.ndarray**: the numeric values in the file
2371
            - **['str']**: list of row names
2372
            - **[`str`]**: list of col_names
2373

2374

2375
        """
2376
        try:
×
2377
            from scipy.io import FortranFile
×
2378
        except Exception as e:
×
2379
            raise Exception("Matrix.from_fortranfile requires scipy")
×
2380
        f = FortranFile(filename, mode="r")
×
2381
        itemp1, itemp2 = f.read_ints()
×
2382
        icount = int(f.read_ints())
×
2383
        if itemp1 >= 0:
×
2384
            raise TypeError(
×
2385
                "Matrix.from_binary(): Jco produced by "
2386
                + "deprecated version of PEST,"
2387
                + "Use JcoTRANS to convert to new format"
2388
            )
2389
        ncol, nrow = abs(itemp1), abs(itemp2)
×
2390
        data = []
×
2391
        for _ in range(icount):
×
2392
            d = f.read_record(Matrix.binary_rec_dt)[0]
×
2393
            data.append(d)
×
2394
        data = np.array(data, dtype=Matrix.binary_rec_dt)
×
2395
        icols = ((data["j"] - 1) // nrow) + 1
×
2396
        irows = data["j"] - ((icols - 1) * nrow)
×
2397
        x = np.zeros((nrow, ncol))
×
2398
        x[irows - 1, icols - 1] = data["dtemp"]
×
2399
        row_names = []
×
2400
        col_names = []
×
2401
        for j in range(ncol):
×
2402
            name = f.read_record("|S12")[0].strip().decode()
×
2403
            col_names.append(name)
×
2404
        # obs_rec = np.dtype((np.str_, self.obs_length))
2405
        for i in range(nrow):
×
2406
            name = f.read_record("|S20")[0].strip().decode()
×
2407
            row_names.append(name)
×
2408
        if len(row_names) != x.shape[0]:
×
2409
            raise Exception(
×
2410
                "Matrix.from_fortranfile() len(row_names) ("
2411
                + str(len(row_names))
2412
                + ") != self.shape[0] ("
2413
                + str(x.shape[0])
2414
                + ")"
2415
            )
2416
        if len(col_names) != x.shape[1]:
×
2417
            raise Exception(
×
2418
                "Matrix.from_fortranfile() len(col_names) ("
2419
                + str(len(col_names))
2420
                + ") != self.shape[1] ("
2421
                + str(x.shape[1])
2422
                + ")"
2423
            )
2424
        # return cls(x=x,row_names=row_names,col_names=col_names)
2425
        return x, row_names, col_names
×
2426

2427
    def to_ascii(self, filename, icode=2):
9✔
2428
        """write a PEST-compatible ASCII Matrix/vector file
2429

2430
        Args:
2431
            filename (`str`): filename to write to
2432
            icode (`int`, optional): PEST-style info code for matrix style.
2433
                Default is 2.
2434
        Note:
2435
            if `icode` == -1, a 1-d  vector is written that represents a diagonal matrix.  An
2436
            exception is raised if `icode` == -1 and `isdiagonal` is False
2437

2438

2439

2440
        """
2441
        if icode == -1:
9✔
2442
            if not self.isdiagonal:
8✔
2443
                raise Exception(
8✔
2444
                    "Matrix.to_ascii(): error: icode supplied as -1 for non-diagonal matrix"
2445
                )
2446
            if self.shape[0] != self.shape[1]:
8✔
2447
                raise Exception(
×
2448
                    "Matrix.to_ascii(): error: icode supplied as -1 for non-square matrix"
2449
                )
2450
        nrow, ncol = self.shape
9✔
2451
        f_out = open(filename, "w")
9✔
2452
        f_out.write(" {0:7.0f} {1:7.0f} {2:7.0f}\n".format(nrow, ncol, icode))
9✔
2453
        f_out.close()
9✔
2454
        f_out = open(filename, "ab")
9✔
2455
        if self.isdiagonal and icode != -1:
9✔
2456
            x = np.diag(self.__x[:, 0])
9✔
2457
        else:
2458
            x = self.__x
9✔
2459
        np.savetxt(f_out, x, fmt="%15.7E", delimiter="")
9✔
2460
        f_out.close()
9✔
2461
        f_out = open(filename, "a")
9✔
2462
        if icode == 1 or icode == -1:
9✔
2463
            f_out.write("* row and column names\n")
9✔
2464
            for r in self.row_names:
9✔
2465
                f_out.write(r + "\n")
9✔
2466
        else:
2467
            f_out.write("* row names\n")
9✔
2468
            for r in self.row_names:
9✔
2469
                f_out.write(r + "\n")
9✔
2470
            f_out.write("* column names\n")
9✔
2471
            for c in self.col_names:
9✔
2472
                f_out.write(c + "\n")
9✔
2473
            f_out.close()
9✔
2474

2475
    @classmethod
9✔
2476
    def from_ascii(cls, filename):
9✔
2477
        """load a PEST-compatible ASCII matrix/vector file into a
2478
        `Matrix` instance
2479

2480
        Args:
2481
            filename (`str`): name of the file to read
2482

2483
        Returns:
2484
            `Matrix`: `Matrix` loaded from ASCII file
2485

2486
        Example::
2487

2488
            mat = pyemu.Matrix.from_ascii("my.mat")
2489
            cov = pyemi.Cov.from_ascii("my.cov")
2490

2491
        """
2492
        x, row_names, col_names, isdiag = Matrix.read_ascii(filename)
9✔
2493
        return cls(x=x, row_names=row_names, col_names=col_names, isdiagonal=isdiag)
9✔
2494

2495
    @staticmethod
9✔
2496
    def read_ascii(filename):
9✔
2497
        """read a PEST-compatible ASCII matrix/vector file
2498

2499
        Args:
2500
            filename (`str`): file to read from
2501

2502
        Returns:
2503
            tuple containing
2504

2505
            - **numpy.ndarray**: numeric values
2506
            - **['str']**: list of row names
2507
            - **[`str`]**: list of column names
2508
            - **bool**: diagonal flag
2509

2510
        """
2511

2512
        f = open(filename, "r")
9✔
2513
        raw = f.readline().strip().split()
9✔
2514
        nrow, ncol = int(raw[0]), int(raw[1])
9✔
2515
        icode = int(raw[2])
9✔
2516
        # x = np.fromfile(f, dtype=self.double, count=nrow * ncol, sep=' ')
2517
        # this painfully slow and ugly read is needed to catch the
2518
        # fortran floating points that have 3-digit exponents,
2519
        # which leave out the base (e.g. 'e') : "-1.23455+300"
2520
        count = 0
9✔
2521
        x = []
9✔
2522
        while True:
4✔
2523
            line = f.readline()
9✔
2524
            if line == "":
9✔
2525
                raise Exception("Matrix.from_ascii() error: EOF")
×
2526
            raw = line.strip().split()
9✔
2527
            for r in raw:
9✔
2528
                try:
9✔
2529
                    x.append(float(r))
9✔
2530
                except Exception as e:
×
2531
                    # overflow
2532
                    if "+" in r:
×
2533
                        x.append(1.0e30)
×
2534
                    # underflow
2535
                    elif "-" in r:
×
2536
                        x.append(0.0)
×
2537
                    else:
2538
                        raise Exception(
×
2539
                            "Matrix.from_ascii() error: "
2540
                            + " can't cast "
2541
                            + r
2542
                            + " to float"
2543
                        )
2544
                count += 1
9✔
2545
                if icode != -1 and count == (nrow * ncol):
9✔
2546
                    break
9✔
2547
                elif icode == -1 and count == nrow:
9✔
2548
                    break
8✔
2549
            if icode != -1 and count == (nrow * ncol):
9✔
2550
                break
9✔
2551
            elif icode == -1 and count == nrow:
9✔
2552
                break
8✔
2553

2554
        x = np.array(x, dtype=Matrix.double)
9✔
2555
        if icode != -1:
9✔
2556
            x.resize(nrow, ncol)
9✔
2557
        else:
2558
            x = np.diag(x)
8✔
2559
        line = f.readline().strip().lower()
9✔
2560
        if not line.startswith("*"):
9✔
2561
            raise Exception(
×
2562
                'Matrix.from_ascii(): error loading ascii file," +\
2563
                "line should start with * not '
2564
                + line
2565
            )
2566
        if "row" in line and "column" in line:
9✔
2567
            if nrow != ncol:
9✔
2568
                raise Exception("nrow != ncol")
×
2569
            names = []
9✔
2570
            for i in range(nrow):
9✔
2571
                line = f.readline().strip().lower()
9✔
2572
                names.append(line)
9✔
2573
            row_names = copy.deepcopy(names)
9✔
2574
            col_names = names
9✔
2575

2576
        else:
2577
            names = []
9✔
2578
            for _ in range(nrow):
9✔
2579
                line = f.readline().strip().lower()
9✔
2580
                names.append(line)
9✔
2581
            row_names = names
9✔
2582
            line = f.readline().strip().lower()
9✔
2583
            if "column" not in line:
9✔
2584
                raise Exception(
×
2585
                    "Matrix.from_ascii(): line should be * column names "
2586
                    + "instead of: "
2587
                    + line
2588
                )
2589
            names = []
9✔
2590
            for _ in range(ncol):
9✔
2591
                line = f.readline().strip().lower()
9✔
2592
                names.append(line)
9✔
2593
            col_names = names
9✔
2594
        f.close()
9✔
2595
        # test for diagonal
2596
        isdiagonal = False
9✔
2597
        if nrow == ncol:
9✔
2598
            diag = np.diag(np.diag(x))
9✔
2599
            diag_tol = 1.0e-6
9✔
2600
            diag_delta = np.abs(diag.sum() - x.sum())
9✔
2601
            if diag_delta < diag_tol:
9✔
2602
                isdiagonal = True
8✔
2603
                x = np.atleast_2d(np.diag(x)).transpose()
8✔
2604
        return x, row_names, col_names, isdiagonal
9✔
2605

2606
    def df(self):
9✔
2607
        """wrapper of Matrix.to_dataframe()"""
2608
        return self.to_dataframe()
8✔
2609

2610
    @classmethod
9✔
2611
    def from_dataframe(cls, df):
9✔
2612
        """class method to create a new `Matrix` instance from a
2613
         `pandas.DataFrame`
2614

2615
        Args:
2616
            df (`pandas.DataFrame`): dataframe
2617

2618
        Returns:
2619
            `Matrix`: `Matrix` instance derived from `df`.
2620

2621
        Example::
2622

2623
            df = pd.read_csv("my.csv")
2624
            mat = pyemu.Matrix.from_dataframe(df)
2625

2626
        """
2627
        if not isinstance(df, pd.DataFrame):
9✔
2628
            raise Exception("df is not a DataFrame")
×
2629
        row_names = copy.deepcopy(list(df.index))
9✔
2630
        col_names = copy.deepcopy(list(df.columns))
9✔
2631
        return cls(x=df.values, row_names=row_names, col_names=col_names)
9✔
2632

2633
    @classmethod
9✔
2634
    def from_names(
9✔
2635
        cls, row_names, col_names, isdiagonal=False, autoalign=True, random=False
2636
    ):
2637
        """class method to create a new Matrix instance from
2638
        row names and column names, filled with trash
2639

2640
        Args:
2641
            row_names (['str']): row names for the new `Matrix`
2642
            col_names (['str']): col_names for the new matrix
2643
            isdiagonal (`bool`, optional): flag for diagonal matrix. Default is False
2644
            autoalign (`bool`, optional): flag for autoaligning new matrix
2645
                during linear algebra calcs. Default is True
2646
            random (`bool`): flag for contents of the trash matrix.
2647
                If True, fill with random numbers, if False, fill with zeros
2648
                Default is False
2649

2650
        Returns:
2651
            `Matrix`: the new Matrix instance
2652

2653
        Example::
2654

2655
            row_names = ["row_1","row_2"]
2656
            col_names = ["col_1,"col_2"]
2657
            m = pyemu.Matrix.from_names(row_names,col_names)
2658

2659

2660
        """
2661
        if random:
8✔
2662
            return cls(
8✔
2663
                x=np.random.random((len(row_names), len(col_names))),
2664
                row_names=row_names,
2665
                col_names=col_names,
2666
                isdiagonal=isdiagonal,
2667
                autoalign=autoalign,
2668
            )
2669
        else:
2670
            return cls(
8✔
2671
                x=np.empty((len(row_names), len(col_names))),
2672
                row_names=row_names,
2673
                col_names=col_names,
2674
                isdiagonal=isdiagonal,
2675
                autoalign=autoalign,
2676
            )
2677

2678
    def to_dataframe(self):
9✔
2679
        """return a pandas.DataFrame representation of `Matrix`
2680

2681
        Returns:
2682
            `pandas.DataFrame`: a dataframe derived from `Matrix`
2683

2684
        Note:
2685
            if `self.isdiagonal` is True, the full matrix is used to fill
2686
            the dataframe - lots of zeros.
2687

2688
        """
2689
        if self.isdiagonal:
9✔
2690
            x = np.diag(self.__x[:, 0])
9✔
2691
        else:
2692
            x = self.__x
9✔
2693
        return pd.DataFrame(data=x, index=self.row_names, columns=self.col_names)
9✔
2694

2695
    def extend(self, other):
9✔
2696
        """extend `Matrix` with the elements of other, returning a new matrix.
2697

2698
        Args:
2699
        other (`Matrix`):  the Matrix to extend self by
2700

2701
        Returns:
2702
            `Matrix`: new, extended `Matrix`
2703

2704
        Note:
2705
            No row or column names can be shared between self and other
2706

2707
        Example::
2708

2709
            jco1 = pyemu.Jco.from_binary("pest_history.jco")
2710
            jco2 = pyemu.Jco.from_binary("pest_forecast.jco")
2711

2712
            jco_ext = jco1.extend(jco2)
2713

2714

2715
        """
2716

2717
        if len(set(self.row_names).intersection(set(other.row_names))) != 0:
8✔
2718
            raise Exception("shared row names")
×
2719
        if len(set(self.col_names).intersection(set(other.col_names))) != 0:
8✔
2720
            raise Exception("shared col_names")
×
2721
        if type(self) != type(other):
8✔
2722
            raise Exception("type mismatch")
×
2723
        new_row_names = copy.copy(self.row_names)
8✔
2724
        new_row_names.extend(other.row_names)
8✔
2725
        new_col_names = copy.copy(self.col_names)
8✔
2726
        new_col_names.extend(other.col_names)
8✔
2727

2728
        new_x = np.zeros((len(new_row_names), len(new_col_names)))
8✔
2729
        new_x[0 : self.shape[0], 0 : self.shape[1]] = self.as_2d
8✔
2730
        new_x[
8✔
2731
            self.shape[0] : self.shape[0] + other.shape[0],
2732
            self.shape[1] : self.shape[1] + other.shape[1],
2733
        ] = other.as_2d
2734
        isdiagonal = True
8✔
2735
        if not self.isdiagonal or not other.isdiagonal:
8✔
2736
            isdiagonal = False
8✔
2737

2738
        return type(self)(
8✔
2739
            x=new_x,
2740
            row_names=new_row_names,
2741
            col_names=new_col_names,
2742
            isdiagonal=isdiagonal,
2743
        )
2744

2745

2746
class Jco(Matrix):
9✔
2747
    """a thin wrapper class to get more intuitive attribute names.  Functions
9✔
2748
    exactly like `Matrix`
2749
    """
2750

2751
    def __init(self, **kwargs):
9✔
2752
        """Jco constuctor takes the same arguments as Matrix.
2753

2754
        Args:
2755
            **kwargs (`dict`): constructor arguments for `Matrix`
2756

2757
        Example:
2758

2759
            jco = pyemu.Jco.from_binary("my.jco")
2760

2761

2762
        """
2763

2764
        super(Jco, self).__init__(kwargs)
×
2765

2766
    @property
9✔
2767
    def par_names(self):
9✔
2768
        """thin wrapper around `Matrix.col_names`
2769

2770
        Returns:
2771
            [`str`]: a list of parameter names
2772

2773
        """
2774
        return self.col_names
9✔
2775

2776
    @property
9✔
2777
    def obs_names(self):
9✔
2778
        """thin wrapper around `Matrix.row_names`
2779

2780
        Returns:
2781
            ['str']: a list of observation names
2782

2783
        """
2784
        return self.row_names
9✔
2785

2786
    @property
9✔
2787
    def npar(self):
9✔
2788
        """number of parameters in the Jco
2789

2790
        Returns:
2791
            `int`: number of parameters (columns)
2792

2793
        """
2794
        return self.shape[1]
×
2795

2796
    @property
9✔
2797
    def nobs(self):
9✔
2798
        """number of observations in the Jco
2799

2800
        Returns:
2801
            `int`: number of observations (rows)
2802

2803
        """
2804
        return self.shape[0]
×
2805

2806
    @classmethod
9✔
2807
    def from_pst(cls, pst, random=False):
9✔
2808
        """construct a new empty Jco from a control file optionally filled
2809
        with trash
2810

2811
        Args:
2812
            pst (`pyemu.Pst`): a pest control file instance.  If type is 'str',
2813
                `pst` is loaded from filename
2814
            random (`bool`): flag for contents of the trash matrix.
2815
                If True, fill with random numbers, if False, fill with zeros
2816
                Default is False
2817

2818
        Returns:
2819
            `Jco`: the new Jco instance
2820

2821
        """
2822

2823
        if isinstance(pst, str):
8✔
2824
            pst = Pst(pst)
8✔
2825

2826
        return Jco.from_names(pst.obs_names, pst.adj_par_names, random=random)
8✔
2827

2828

2829
class Cov(Matrix):
9✔
2830
    """Diagonal and/or dense Covariance matrices
9✔
2831

2832
    Args:
2833
        x (`numpy.ndarray`): numeric values
2834
        names ([`str`]): list of row and column names
2835
        isdigonal (`bool`): flag if the Matrix is diagonal
2836
        autoalign (`bool`): flag to control the autoalignment of Matrix during
2837
            linear algebra operations
2838

2839
    Example::
2840

2841
        data = np.random.random((10,10))
2842
        names = ["par_{0}".format(i) for i in range(10)]
2843
        mat = pyemu.Cov(x=data,names=names)
2844
        mat.to_binary("mat.jco")
2845

2846
    Note:
2847
        `row_names` and `col_names` args are supported in the contructor
2848
        so support inheritance.  However, users should only pass `names`
2849

2850
    """
2851

2852
    def __init__(
9✔
2853
        self,
2854
        x=None,
2855
        names=[],
2856
        row_names=[],
2857
        col_names=[],
2858
        isdiagonal=False,
2859
        autoalign=True,
2860
    ):
2861

2862
        self.__identity = None
9✔
2863
        self.__zero = None
9✔
2864
        # if len(row_names) > 0 and len(col_names) > 0:
2865
        #    assert row_names == col_names
2866
        self.__identity = None
9✔
2867
        self.__zero = None
9✔
2868
        # if len(row_names) > 0 and len(col_names) > 0:
2869
        #    assert row_names == col_names
2870
        if len(names) != 0 and len(row_names) == 0:
9✔
2871
            row_names = names
9✔
2872
        if len(names) != 0 and len(col_names) == 0:
9✔
2873
            col_names = names
9✔
2874
        super(Cov, self).__init__(
9✔
2875
            x=x,
2876
            isdiagonal=isdiagonal,
2877
            row_names=row_names,
2878
            col_names=col_names,
2879
            autoalign=autoalign,
2880
        )
2881
        super(Cov, self).__init__(
9✔
2882
            x=x,
2883
            isdiagonal=isdiagonal,
2884
            row_names=row_names,
2885
            col_names=col_names,
2886
            autoalign=autoalign,
2887
        )
2888

2889
    @property
9✔
2890
    def identity(self):
9✔
2891
        """get an identity `Cov` of the same shape
2892

2893
        Returns:
2894
            `Cov`: new `Cov` instance with identity matrix
2895

2896
        Note:
2897
            the returned identity matrix has the same row-col names as self
2898

2899
        """
2900
        if self.__identity is None:
9✔
2901
            self.__identity = Cov(
9✔
2902
                x=np.atleast_2d(np.ones(self.shape[0])).transpose(),
2903
                names=self.row_names,
2904
                isdiagonal=True,
2905
            )
2906
        return self.__identity
9✔
2907

2908
    @property
9✔
2909
    def zero(self):
9✔
2910
        """get a diagonal instance of `Cov` with all zeros on the diagonal
2911

2912
        Returns:
2913
            `Cov`: new `Cov` instance with zeros
2914

2915
        """
2916
        if self.__zero is None:
×
2917
            self.__zero = Cov(
×
2918
                x=np.atleast_2d(np.zeros(self.shape[0])).transpose(),
2919
                names=self.row_names,
2920
                isdiagonal=True,
2921
            )
2922
        return self.__zero
×
2923

2924
    def condition_on(self, conditioning_elements):
9✔
2925
        """get a new Covariance object that is conditional on knowing some
2926
        elements.  uses Schur's complement for conditional Covariance
2927
        propagation
2928

2929
        Args:
2930
            conditioning_elements (['str']): list of names of elements to condition on
2931

2932
        Returns:
2933
            `Cov`: new conditional `Cov` that assumes `conditioning_elements` have become known
2934

2935
        Example::
2936

2937
            prior_cov = pyemu.Cov.from_parameter_data(pst)
2938
            now_known_pars = pst.adj_par_names[:5]
2939
            post_cov = prior_cov.condition_on(now_known_pars)
2940

2941

2942
        """
2943
        if not isinstance(conditioning_elements, list):
9✔
2944
            conditioning_elements = [conditioning_elements]
×
2945
        for iname, name in enumerate(conditioning_elements):
9✔
2946
            conditioning_elements[iname] = name.lower()
9✔
2947
            if name.lower() not in self.col_names:
9✔
2948
                raise Exception("Cov.condition_on() name not found: " + name)
×
2949
        keep_names = []
9✔
2950
        for name in self.col_names:
9✔
2951
            if name not in conditioning_elements:
9✔
2952
                keep_names.append(name)
9✔
2953
        # C11
2954
        new_Cov = self.get(keep_names)
9✔
2955
        if self.isdiagonal:
9✔
2956
            return new_Cov
9✔
2957
        # C22^1
2958
        cond_Cov = self.get(conditioning_elements).inv
×
2959
        # C12
2960
        upper_off_diag = self.get(keep_names, conditioning_elements)
×
2961
        # print(new_Cov.shape,upper_off_diag.shape,cond_Cov.shape)
2962
        return new_Cov - (upper_off_diag * cond_Cov * upper_off_diag.T)
×
2963

2964
    @property
9✔
2965
    def names(self):
9✔
2966
        """wrapper for getting row_names.  row_names == col_names for Cov
2967

2968
        Returns:
2969
            [`str`]: list of names
2970

2971
        """
2972
        return self.row_names
9✔
2973

2974
    def replace(self, other):
9✔
2975
        """replace elements in the covariance matrix with elements from other.
2976
        if other is not diagonal, then this `Cov` becomes non diagonal
2977

2978
        Args:
2979
            `Cov`: the Cov to replace elements in this `Cov` with
2980

2981
        Note:
2982
            operates in place.  Other must have the same row-col names as self
2983

2984
        """
2985
        if not isinstance(other, Cov):
9✔
2986
            raise Exception(
×
2987
                "Cov.replace() other must be Cov, not {0}".format(type(other))
2988
            )
2989
        # make sure the names of other are in self
2990
        missing = [n for n in other.names if n not in self.names]
9✔
2991
        if len(missing) > 0:
9✔
2992
            raise Exception(
×
2993
                "Cov.replace(): the following other names are not"
2994
                + " in self names: {0}".format(",".join(missing))
2995
            )
2996
        self_idxs = self.indices(other.names, 0)
9✔
2997
        other_idxs = other.indices(other.names, 0)
9✔
2998

2999
        if self.isdiagonal and other.isdiagonal:
9✔
3000
            self._Matrix__x[self_idxs] = other.x[other_idxs]
8✔
3001
            return
8✔
3002
        if self.isdiagonal:
9✔
3003
            self._Matrix__x = self.as_2d
9✔
3004
            self.isdiagonal = False
9✔
3005

3006
        # print("allocating other_x")
3007
        other_x = other.as_2d
9✔
3008
        # print("replacing")
3009
        for i, ii in zip(self_idxs, other_idxs):
9✔
3010
            self._Matrix__x[i, self_idxs] = other_x[ii, other_idxs].copy()
9✔
3011
        # print("resetting")
3012
        # self.reset_x(self_x)
3013
        # self.isdiagonal = False
3014

3015
    def to_uncfile(
9✔
3016
        self, unc_file, covmat_file="cov.mat", var_mult=1.0, include_path=False
3017
    ):
3018
        """write a PEST-compatible uncertainty file
3019

3020
        Args:
3021
            unc_file (`str`): filename of the uncertainty file
3022
            covmat_file (`str`): covariance matrix filename. Default is
3023
                "Cov.mat".  If None, and Cov.isdiaonal, then a standard deviation
3024
                form of the uncertainty file is written.  Exception raised if `covmat_file` is `None`
3025
                and not `Cov.isdiagonal`
3026
            var_mult (`float`): variance multiplier for the covmat_file entry
3027
            include_path (`bool`): flag to include the path of `unc_file` in the name of `covmat_file`.
3028
                Default is False - not sure why you would ever make this True...
3029

3030
        Example::
3031

3032
            cov = pyemu.Cov.from_parameter_data(pst)
3033
            cov.to_uncfile("my.unc")
3034

3035
        """
3036
        assert (
9✔
3037
            len(self.row_names) == self.shape[0]
3038
        ), "Cov.to_uncfile(): len(row_names) != x.shape[0] "
3039
        if len(self.row_names) != self.shape[0]:
9✔
3040
            raise Exception("Cov.to_uncfile(): len(row_names) != x.shape[0]")
×
3041
        if covmat_file:
9✔
3042
            f = open(unc_file, "w")
9✔
3043
            f.write("START COVARIANCE_MATRIX\n")
9✔
3044
            if include_path:
9✔
3045
                f.write(" file " + covmat_file + "\n")
×
3046
            else:
3047
                f.write(" file " + os.path.split(covmat_file)[-1] + "\n")
9✔
3048
            f.write(" variance_multiplier {0:15.6E}\n".format(var_mult))
9✔
3049
            f.write("END COVARIANCE_MATRIX\n")
9✔
3050
            f.close()
9✔
3051
            if include_path:
9✔
3052
                self.to_ascii(covmat_file, icode=1)
×
3053
            else:
3054
                self.to_ascii(
9✔
3055
                    os.path.join(
3056
                        os.path.dirname(unc_file), os.path.split(covmat_file)[-1]
3057
                    ),
3058
                    icode=1,
3059
                )
3060

3061
        else:
3062
            if self.isdiagonal:
9✔
3063
                f = open(unc_file, "w")
9✔
3064
                f.write("START STANDARD_DEVIATION\n")
9✔
3065
                for iname, name in enumerate(self.row_names):
9✔
3066
                    f.write(
9✔
3067
                        "  {0:20s}  {1:15.6E}\n".format(name, np.sqrt(self.x[iname, 0]))
3068
                    )
3069
                f.write("END STANDARD_DEVIATION\n")
9✔
3070
                f.close()
9✔
3071
            else:
3072
                raise Exception(
×
3073
                    "Cov.to_uncfile(): can't write non-diagonal "
3074
                    + "object as standard deviation block"
3075
                )
3076

3077
    @classmethod
9✔
3078
    def from_obsweights(cls, pst_file):
9✔
3079
        """instantiates a `Cov` instance from observation weights in
3080
        a PEST control file.
3081

3082
        Args:
3083
            pst_file (`str`): pest control file name
3084

3085
        Returns:
3086
            `Cov`: a diagonal observation noise covariance matrix derived from the
3087
            weights in the pest control file.  Zero-weighted observations
3088
            are included with a weight of 1.0e-30
3089

3090
        Note:
3091
            Calls `Cov.from_observation_data()`
3092

3093
        Example::
3094

3095
            obscov = pyemu.Cov.from_obsweights("my.pst")
3096

3097

3098
        """
3099
        if not pst_file.endswith(".pst"):
9✔
3100
            pst_file += ".pst"
×
3101
        return Cov.from_observation_data(Pst(pst_file))
9✔
3102

3103
    @classmethod
9✔
3104
    def from_observation_data(cls, pst):
9✔
3105
        """instantiates a `Cov` from pyemu.Pst.observation_data
3106

3107
        Args:
3108
            pst (`pyemu.Pst`): control file instance
3109

3110
        Returns:
3111
            `Cov`: a diagonal observation noise covariance matrix derived from the
3112
            weights in the pest control file.  Zero-weighted observations
3113
            are included with a weight of 1.0e-30
3114

3115
        Example::
3116

3117
            obscov = pyemu.Cov.from_observation_data(pst)
3118

3119
        """
3120
        nobs = pst.observation_data.shape[0]
9✔
3121
        x = np.zeros((nobs, 1))
9✔
3122
        onames = []
9✔
3123
        ocount = 0
9✔
3124
        std_dict = {}
9✔
3125
        if "standard_deviation" in pst.observation_data.columns:
9✔
3126
            std_dict = pst.observation_data.standard_deviation.to_dict()
8✔
3127
            std_dict = {o:s**2 for o,s in std_dict.items() if pd.notna(s)}
8✔
3128
        for weight, obsnme in zip(
9✔
3129
            pst.observation_data.weight, pst.observation_data.obsnme
3130
        ):
3131
            w = float(weight)
9✔
3132
            w = max(w, 1.0e-30)
9✔
3133

3134
            x[ocount] = std_dict.get(obsnme,(1.0 / w) ** 2)
9✔
3135
            ocount += 1
9✔
3136
            onames.append(obsnme.lower())
9✔
3137
        return cls(x=x, names=onames, isdiagonal=True)
9✔
3138

3139
    @classmethod
9✔
3140
    def from_parbounds(cls, pst_file, sigma_range=4.0, scale_offset=True):
9✔
3141
        """Instantiates a `Cov` from a pest control file parameter data section using
3142
        parameter bounds as a proxy for uncertainty.
3143

3144

3145
        Args:
3146
            pst_file (`str`): pest control file name
3147
            sigma_range (`float`): defines range of upper bound - lower bound in terms of standard
3148
                deviation (sigma). For example, if sigma_range = 4, the bounds
3149
                represent 4 * sigma.  Default is 4.0, representing approximately
3150
                95% confidence of implied normal distribution
3151
            scale_offset (`bool`): flag to apply scale and offset to parameter upper and lower
3152
                bounds before calculating varaince. In some cases, not applying scale and
3153
                offset can result in undefined (log) variance.  Default is True.
3154

3155
        Returns:
3156
            `Cov`: diagonal parameter `Cov` matrix created from parameter bounds
3157

3158
        Note:
3159
            Calls `Cov.from_parameter_data()`
3160

3161
        """
3162
        if not pst_file.endswith(".pst"):
9✔
3163
            pst_file += ".pst"
×
3164
        new_pst = Pst(pst_file)
9✔
3165
        return Cov.from_parameter_data(new_pst, sigma_range, scale_offset)
9✔
3166

3167
    @classmethod
9✔
3168
    def from_parameter_data(cls, pst, sigma_range=4.0, scale_offset=True,
9✔
3169
                            subset=None):
3170
        """Instantiates a `Cov` from a pest control file parameter data section using
3171
        parameter bounds as a proxy for uncertainty.
3172

3173

3174
        Args:
3175
            pst_file (`str`): pest control file name
3176
            sigma_range (`float`): defines range of upper bound - lower bound in terms of standard
3177
                deviation (sigma). For example, if sigma_range = 4, the bounds
3178
                represent 4 * sigma.  Default is 4.0, representing approximately
3179
                95% confidence of implied normal distribution
3180
            scale_offset (`bool`): flag to apply scale and offset to parameter upper and lower
3181
                bounds before calculating varaince. In some cases, not applying scale and
3182
                offset can result in undefined (log) variance.  Default is True.
3183
            subset (`list`-like, optional): Subset of parameters to draw
3184

3185
        Returns:
3186
            `Cov`: diagonal parameter `Cov` matrix created from parameter bounds
3187

3188
        Note:
3189
            Calls `Cov.from_parameter_data()`
3190

3191
        """
3192
        if subset is not None:
9✔
3193
            missing = subset.difference(pst.par_names)
9✔
3194
            if not missing.empty:
9✔
3195
                warnings.warn(
×
3196
                    f"{len(missing)} parameter names not present in Pst:\n"
3197
                    f"{missing}", PyemuWarning
3198
                )
3199
                subset = subset.intersection(pst.par_names)
×
3200
            par_dat = pst.parameter_data.loc[subset, :]
9✔
3201
        else:
3202
            par_dat = pst.parameter_data
9✔
3203
        npar = (~par_dat.partrans.isin(["fixed", "tied"])).sum()
9✔
3204
        x = np.zeros((npar, 1))
9✔
3205
        names = []
9✔
3206
        idx = 0
9✔
3207
        for i, row in par_dat.iterrows():
9✔
3208
            t = row["partrans"]
9✔
3209
            if t in ["fixed", "tied"]:
9✔
3210
                continue
9✔
3211
            if "standard_deviation" in row.index and pd.notna(row["standard_deviation"]):
9✔
3212
                if t == "log":
×
3213
                    var = (np.log10(row["standard_deviation"])) ** 2
×
3214
                else:
3215
                    var = row["standard_deviation"] ** 2
×
3216
            else:
3217
                if scale_offset:
9✔
3218
                    lb = row.parlbnd * row.scale + row.offset
9✔
3219
                    ub = row.parubnd * row.scale + row.offset
9✔
3220
                else:
3221
                    lb = row.parlbnd
9✔
3222
                    ub = row.parubnd
9✔
3223

3224
                if t == "log":
9✔
3225
                    var = ((np.log10(np.abs(ub)) - np.log10(np.abs(lb))) / sigma_range) ** 2
9✔
3226
                else:
3227
                    var = ((ub - lb) / sigma_range) ** 2
8✔
3228
            if np.isnan(var) or not np.isfinite(var):
9✔
3229
                raise Exception(
×
3230
                    "Cov.from_parameter_data() error: "
3231
                    + "variance for parameter {0} is nan".format(row["parnme"])
3232
                )
3233
            if var == 0.0:
9✔
3234
                s = (
×
3235
                    "Cov.from_parameter_data() error: "
3236
                    + "variance for parameter {0} is 0.0.".format(row["parnme"])
3237
                )
3238
                s += "  This might be from enforcement of scale/offset and log transform."
×
3239
                s += "  Try changing 'scale_offset' arg"
×
3240
                raise Exception(s)
×
3241
            x[idx] = var
9✔
3242
            names.append(row["parnme"].lower())
9✔
3243
            idx += 1
9✔
3244

3245
        return cls(x=x, names=names, isdiagonal=True)
9✔
3246

3247
    @classmethod
9✔
3248
    def from_uncfile(cls, filename, pst=None):
9✔
3249
        """instaniates a `Cov` from a PEST-compatible uncertainty file
3250

3251
        Args:
3252
            filename (`str`):  uncertainty file name
3253
            pst ('pyemu.Pst`): a control file instance.  this is needed if
3254
                "first_parameter" and "last_parameter" keywords.  Default is None
3255

3256
        Returns:
3257
            `Cov`: `Cov` instance from uncertainty file
3258

3259
        Example::
3260

3261
            cov = pyemu.Cov.from_uncfile("my.unc")
3262

3263
        """
3264

3265
        if pst is not None:
9✔
3266
            if isinstance(pst, str):
8✔
3267
                pst = Pst(pst)
×
3268

3269
        nentries = Cov._get_uncfile_dimensions(filename)
9✔
3270
        x = np.zeros((nentries, nentries))
9✔
3271
        row_names = []
9✔
3272
        col_names = []
9✔
3273
        f = open(filename, "r")
9✔
3274
        isdiagonal = True
9✔
3275
        idx = 0
9✔
3276
        while True:
4✔
3277
            line = f.readline().lower()
9✔
3278
            if len(line) == 0:
9✔
3279
                break
9✔
3280
            line = line.strip()
9✔
3281
            if "start" in line:
9✔
3282
                if "pest_control_file" in line:
9✔
3283
                    raise Exception(
×
3284
                        "Cov.from_uncfile() error: 'pest_control_file' block not supported"
3285
                    )
3286

3287
                if "standard_deviation" in line:
9✔
3288
                    std_mult = 1.0
8✔
3289
                    while True:
4✔
3290
                        line2 = f.readline().strip().lower()
8✔
3291
                        if line2.strip().lower().startswith("end"):
8✔
3292
                            break
8✔
3293

3294
                        raw = line2.strip().split()
8✔
3295
                        name, val = raw[0], float(raw[1])
8✔
3296
                        if name == "std_multiplier":
8✔
3297
                            std_mult = val
×
3298
                        else:
3299
                            x[idx, idx] = (val * std_mult) ** 2
8✔
3300
                            if name in row_names:
8✔
3301
                                raise Exception(
×
3302
                                    "Cov.from_uncfile():"
3303
                                    + "duplicate name: "
3304
                                    + str(name)
3305
                                )
3306
                            row_names.append(name)
8✔
3307
                            col_names.append(name)
8✔
3308
                            idx += 1
8✔
3309

3310
                elif "covariance_matrix" in line:
9✔
3311
                    isdiagonal = False
9✔
3312
                    var = 1.0
9✔
3313
                    first_par = None
9✔
3314
                    last_par = None
9✔
3315
                    while True:
4✔
3316
                        line2 = f.readline().strip().lower()
9✔
3317
                        if line2.strip().lower().startswith("end"):
9✔
3318
                            break
9✔
3319
                        if line2.startswith("file"):
9✔
3320
                            mat_filename = os.path.join(
9✔
3321
                                os.path.dirname(filename),
3322
                                line2.split()[1].replace("'", "").replace('"', ""),
3323
                            )
3324
                            cov = Matrix.from_ascii(mat_filename)
9✔
3325

3326
                        elif line2.startswith("variance_multiplier"):
9✔
3327
                            var = float(line2.split()[1])
9✔
3328
                        elif line2.startswith("first_parameter"):
8✔
3329
                            if pst is None:
8✔
3330
                                raise Exception(
8✔
3331
                                    "Cov.from_uncfile(): 'first_parameter' usage requires the 'pst' arg to be passed"
3332
                                )
3333
                            first_par = line2.split()[1]
8✔
3334
                        elif line2.startswith("last_parameter"):
8✔
3335
                            if pst is None:
8✔
3336
                                raise Exception(
×
3337
                                    "Cov.from_uncfile(): 'last_parameter' usage requires the 'pst' arg to be passed"
3338
                                )
3339
                            last_par = line2.split()[1]
8✔
3340

3341
                        else:
3342
                            raise Exception(
×
3343
                                "Cov.from_uncfile(): "
3344
                                + "unrecognized keyword in"
3345
                                + "std block: "
3346
                                + line2
3347
                            )
3348
                    if var != 1.0:
9✔
3349
                        cov *= var
8✔
3350
                    if first_par is not None:
9✔
3351
                        if last_par is None:
8✔
3352
                            raise Exception(
×
3353
                                "'first_par' found but 'last_par' not found"
3354
                            )
3355
                        if first_par not in pst.par_names:
8✔
3356
                            raise Exception(
×
3357
                                "'first_par' {0} not found in pst.par_names".format(
3358
                                    first_par
3359
                                )
3360
                            )
3361
                        if last_par not in pst.par_names:
8✔
3362
                            raise Exception(
×
3363
                                "'last_par' {0} not found in pst.par_names".format(
3364
                                    last_par
3365
                                )
3366
                            )
3367
                        names = pst.parameter_data.loc[
8✔
3368
                            first_par:last_par, "parnme"
3369
                        ].tolist()
3370
                        if len(names) != cov.shape[0]:
8✔
3371
                            print(names)
×
3372
                            print(len(names), cov.shape)
×
3373
                            raise Exception(
×
3374
                                "the number of par names between 'first_par' and "
3375
                                "'last_par' != elements in the cov matrix {0}".format(
3376
                                    mat_filename
3377
                                )
3378
                            )
3379
                        cov.row_names = names
8✔
3380
                        cov.col_names = names
8✔
3381

3382
                    for name in cov.row_names:
9✔
3383
                        if name in row_names:
9✔
3384
                            raise Exception(
×
3385
                                "Cov.from_uncfile():" + " duplicate name: " + str(name)
3386
                            )
3387
                    row_names.extend(cov.row_names)
9✔
3388
                    col_names.extend(cov.col_names)
9✔
3389

3390
                    for i in range(cov.shape[0]):
9✔
3391
                        x[idx + i, idx : idx + cov.shape[0]] = cov.x[i, :].copy()
9✔
3392
                    idx += cov.shape[0]
9✔
3393
                else:
3394
                    raise Exception(
×
3395
                        "Cov.from_uncfile(): " + "unrecognized block:" + str(line)
3396
                    )
3397
        f.close()
9✔
3398
        if isdiagonal:
9✔
3399
            x = np.atleast_2d(np.diag(x)).transpose()
8✔
3400
        return cls(x=x, names=row_names, isdiagonal=isdiagonal)
9✔
3401

3402
    @staticmethod
9✔
3403
    def _get_uncfile_dimensions(filename):
9✔
3404
        """quickly read an uncertainty file to find the dimensions"""
3405
        f = open(filename, "r")
9✔
3406
        nentries = 0
9✔
3407
        while True:
4✔
3408
            line = f.readline().lower()
9✔
3409
            if len(line) == 0:
9✔
3410
                break
9✔
3411
            line = line.strip()
9✔
3412
            if "start" in line:
9✔
3413
                if "standard_deviation" in line:
9✔
3414
                    while True:
4✔
3415
                        line2 = f.readline().strip().lower()
8✔
3416
                        if "std_multiplier" in line2:
8✔
3417
                            continue
×
3418
                        if line2.strip().lower().startswith("end"):
8✔
3419
                            break
8✔
3420
                        nentries += 1
8✔
3421

3422
                elif "covariance_matrix" in line:
9✔
3423
                    while True:
4✔
3424
                        line2 = f.readline().strip().lower()
9✔
3425
                        if line2 == "":
9✔
3426
                            raise Exception("EOF while looking for 'end' block")
×
3427
                        if line2.strip().lower().startswith("end"):
9✔
3428
                            break
9✔
3429
                        if line2.startswith("file"):
9✔
3430
                            mat_filename = os.path.join(
9✔
3431
                                os.path.dirname(filename),
3432
                                line2.split()[1].replace("'", "").replace('"', ""),
3433
                            )
3434
                            cov = Matrix.from_ascii(mat_filename)
9✔
3435
                            nentries += len(cov.row_names)
9✔
3436
                        elif line2.startswith("variance_multiplier"):
9✔
3437
                            pass
9✔
3438
                        elif line2.startswith("first_parameter"):
8✔
3439
                            pass
8✔
3440
                        elif line2.startswith("last_parameter"):
8✔
3441
                            pass
8✔
3442

3443
                        else:
3444
                            raise Exception(
×
3445
                                "Cov.get_uncfile_dimensions(): "
3446
                                + "unrecognized keyword in Covariance block: "
3447
                                + line2
3448
                            )
3449
                else:
3450
                    raise Exception(
×
3451
                        "Cov.get_uncfile_dimensions():"
3452
                        + "unrecognized block:"
3453
                        + str(line)
3454
                    )
3455
        f.close()
9✔
3456
        return nentries
9✔
3457

3458
    @classmethod
9✔
3459
    def identity_like(cls, other):
9✔
3460
        """Get an identity matrix Cov instance like other `Cov`
3461

3462
        Args:
3463
            other (`Matrix`):  other matrix - must be square
3464

3465
        Returns:
3466
            `Cov`: new identity matrix `Cov` with shape of `other`
3467

3468
        Note:
3469
            the returned identity cov matrix is treated as non-diagonal
3470

3471
        """
3472
        if other.shape[0] != other.shape[1]:
8✔
3473
            raise Exception("not square")
×
3474
        x = np.identity(other.shape[0])
8✔
3475
        return cls(x=x, names=other.row_names, isdiagonal=False)
8✔
3476

3477
    def to_pearson(self):
9✔
3478
        """Convert Cov instance to Pearson correlation coefficient
3479
        matrix
3480

3481
        Returns:
3482
            `Matrix`: A `Matrix` of correlation coefs.  Return type is `Matrix`
3483
            on purpose so that it is clear the returned instance is not a Cov
3484

3485
        Example::
3486

3487
            # plot the posterior parameter correlation matrix
3488
            import matplotlib.pyplot as plt
3489
            cov = pyemu.Cov.from_ascii("pest.post.cov")
3490
            cc = cov.to_pearson()
3491
            cc.x[cc.x==1.0] = np.NaN
3492
            plt.imshow(cc)
3493

3494
        """
3495
        std_dict = (
9✔
3496
            self.get_diagonal_vector().to_dataframe()["diag"].apply(np.sqrt).to_dict()
3497
        )
3498
        pearson = self.identity.as_2d
9✔
3499
        if self.isdiagonal:
9✔
3500
            return Matrix(x=pearson, row_names=self.row_names, col_names=self.col_names)
×
3501
        df = self.to_dataframe()
9✔
3502
        # fill the lower triangle
3503
        for i, iname in enumerate(self.row_names):
9✔
3504
            for j, jname in enumerate(self.row_names[i + 1 :]):
9✔
3505
                # cv = df.loc[iname,jname]
3506
                # std1,std2 = std_dict[iname],std_dict[jname]
3507
                # cc = cv / (std1*std2)
3508
                # v1 = np.sqrt(df.loc[iname,iname])
3509
                # v2 = np.sqrt(df.loc[jname,jname])
3510
                pearson[i, j + i + 1] = df.loc[iname, jname] / (
9✔
3511
                    std_dict[iname] * std_dict[jname]
3512
                )
3513

3514
        # replicate across diagonal
3515
        for i, iname in enumerate(self.row_names[:-1]):
9✔
3516
            pearson[i + 1 :, i] = pearson[i, i + 1 :]
9✔
3517
        return Matrix(x=pearson, row_names=self.row_names, col_names=self.col_names)
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc