<commit_before># -*- coding: utf-8 -*-
"""
This module defines the :class:`~pvmismatch.pvmismatch_lib.pvmodule.PVmodule`.
"""

import numpy as np
from copy import copy
from matplotlib import pyplot as plt
# use absolute imports instead of relative, so modules are portable
from pvmismatch.pvmismatch_lib.pvconstants import PVconstants, get_series_cells
from pvmismatch.pvmismatch_lib.pvcell import PVcell

VBYPASS = -0.5  # [V] trigger voltage of bypass diode

def standard_cellpos_pat(nrows, ncols_per_substr):
    cellpos = []
    ncols = [0, 0]
    for substr_cols in ncols_per_substr:
        ncols[0], ncols[1] = ncols[1], ncols[1] + substr_cols
        newsubstr = []
        for col in xrange(*ncols):
            newrow = []
            for row in xrange(nrows):
                idx = col * nrows
                if col % 2 == 0:
                    idx += row
                else:
                    idx += (nrows - row - 1)
                newrow.append({'circuit': 'series', 'idx': idx})
            newsubstr.append(newrow)
        cellpos.append(newsubstr)
    return cellpos

# standard cell positions presets
STD72 = standard_cellpos_pat(12, [2, 2, 2])
STD96 = standard_cellpos_pat(12, [2, 4, 2])
STD128 = standard_cellpos_pat(16, [2, 4, 2])

def crosstied_cellpos_pat(nrows_per_substrs, ncols, partial=False):
    trows = sum(nrows_per_substrs)
    cellpos = []
    nrows = [0, 0]
    for substr_row in nrows_per_substrs:
        nrows[0], nrows[1] = nrows[1], nrows[1] + substr_row
        newsubstr = []
        for col in xrange(ncols):
            newrow = []
            for row in xrange(*nrows):
                circuit = 'parallel'
                if partial and newrow:
                    circuit = 'series'
                newrow.append({'circuit': circuit, 'idx': col * trows + row})
            newsubstr.append(newrow)
        cellpos.append(newsubstr)
    return cellpos

# crosstied cell positions presets
TCT96 = crosstied_cellpos_pat([4, 4, 4], 8)
PCT96 = crosstied_cellpos_pat([4, 4, 4], 8, partial=True)

class PVmodule(object):
    """
    PVmodule - A Class for PV modules.

    :param cell_pos: cell position pattern
    :type cell_pos: dict
    :param pvcells: list of :class:`~pvmismatch.pvmismatch_lib.pvcell.PVcell`
    :type pvcells: list
    :param pvconst: An object with common parameters and constants.
    :type pvconst: :class:`~pvmismatch.pvmismatch_lib.pvconstants.PVconstants`
    :param Vbypass: bypass diode trigger voltage [V]
    :type Vbypass: float
    """
    def __init__(self, cell_pos=STD96, pvcells=None, pvconst=PVconstants(),
                 Vbypass=VBYPASS):
        # TODO: check cell position pattern
        self.cell_pos = cell_pos  #: cell position pattern dictionary
        self.numberCells = sum([len(c) for s in self.cell_pos for c in s])
        """number of cells in the module"""
        self.pvconst = pvconst  #: configuration constants
        self.Vbypass = Vbypass  #: [V] trigger voltage of bypass diode
        if pvcells is None:
            # faster to use copy instead of making each object in a for-loop
            # use copy instead of deepcopy to keey same pvconst for all objects
            # PVcell.calcCell() creates new np.ndarray if attributes change
            pvc = PVcell(pvconst=pvconst)
            pvcells = [copy(pvc) for _ in xrange(self.numberCells)]
        else:
            if len(pvcells) != self.numberCells:
                # TODO: use pvexception
                raise Exception(
                    "Number of cells doesn't match cell position pattern."
                )
        self.pvcells = pvcells  #: list of `PVcell` objects in this `PVmodule`
        self.numSubStr = len(self.cell_pos)  #: number of substrings
        self.subStrCells = [len(_) for _ in self.cell_pos]  #: cells per substr
        # initialize members so PyLint doesn't get upset
        self.Imod, self.Vmod, self.Pmod, self.Vsubstr = self.calcMod()
        # self.setSuns(Ee)

    # copy some values from cells to modules
    @property
    def Ee(self):
        return np.array([pvc.Ee.flatten() for pvc in self.pvcells])

    @property
    def Tcell(self):
        return np.array([pvc.Tcell.flatten() for pvc in self.pvcells])

    @property
    def Icell(self):
        return np.array([pvc.Icell.flatten() for pvc in self.pvcells])

    @property
    def Vcell(self):
        return np.array([pvc.Vcell.flatten() for pvc in self.pvcells])

    @property
    def Pcell(self):
        return np.array([pvc.Pcell.flatten() for pvc in self.pvcells])

    @property
    def Isc(self):
        return np.array([pvc.Isc.flatten() for pvc in self.pvcells])

    @property
    def Voc(self):
        return np.array([pvc.Voc.flatten() for pvc in self.pvcells])

    @property
    def VRBD(self):
        return np.array([pvc.VRBD.flatten() for pvc in self.pvcells])

    def setSuns(self, Ee, cells=None):
        """
        Set the irradiance in suns, Ee, on the solar cells in the module.
        Recalculates cell current (Icell [A]), voltage (Vcell [V]) and power
        (Pcell [W]) as well as module current (Imod [A]), voltage (Vmod [V])
        and power (Pmod [W]).
        Arguments
            Ee : <float> or <np.array of floats> Effective Irradiance
        Optional
            cells : <np.array of int> Cells to change
        """
        if cells is None:
            if np.isscalar(Ee):
                for pvc in self.pvcells:
                    pvc.Ee = Ee
            elif np.size(Ee) == self.numberCells:
                for pvc, Ee_idx in zip(self.pvcells, Ee):
                    pvc.Ee = Ee_idx
            else:
                raise Exception("Input irradiance value (Ee) for each cell!")
        else:
            Nsuns = np.size(cells)
            if np.isscalar(Ee):
                for cell_idx in cells:
                    self.pvcells[cell_idx].Ee = Ee
            elif np.size(Ee) == Nsuns:
                for cell_idx in cells:
                    self.pvcells[cell_idx].Ee = Ee[cell_idx]
            else:
                raise Exception("Input irradiance value (Ee) for each cell!")
        self.Imod, self.Vmod, self.Pmod, self.Vsubstr = self.calcMod()

    def calcMod(self):
        """
        Calculate module I-V curves.
        Returns (Imod, Vmod, Pmod) : tuple of numpy.ndarray of float
        """
        # iterate over substrings
        # TODO: benchmark speed difference append() vs preallocate space
        Isubstr, Vsubstr, Isc_substr, Imax_substr = [], [], [], []
        for substr in self.cell_pos:
            # check if cells are in series or any parallel circuits
            if all(r['circuit'] == 'series' for c in substr for r in c):
                idxs = [r['idx'] for c in substr for r in c]
                IatVrbd = np.asarray(
                    [np.interp(vrbd, v, i) for vrbd, v, i in
                     zip(self.VRBD[idxs], self.Vcell[idxs], self.Icell[idxs])]
                )
                Isub, Vsub = self.pvconst.calcSeries(
                    self.Icell[idxs], self.Vcell[idxs], self.Isc[idxs].mean(),
                    IatVrbd.max()
                )
            elif all(r['circuit'] == 'parallel' for c in substr for r in c):
                Irows, Vrows = [], []
                Isc_rows, Imax_rows = [], []
                for row in zip(*substr):
                    idxs = [c['idx'] for c in row]
                    Irow, Vrow = self.pvconst.calcParallel(
                        self.Icell[idxs], self.Vcell[idxs],
                        self.Voc[idxs].mean(), self.VRBD.min()
                    )
                    Irows.append(Irow)
                    Vrows.append(Vrow)
                    Isc_rows.append(np.interp(0., Vrow, Irow))
                    Imax_rows.append(Irow.max())
                Irows, Vrows = np.asarray(Irows), np.asarray(Vrows)
                Isc_rows = np.asarray(Isc_rows)
                Imax_rows = np.asarray(Imax_rows)
                Isub, Vsub = self.pvconst.calcSeries(
                    Irows, Vrows, Isc_rows.mean(), Imax_rows.max()
                )
            else:
                IVall_cols = []
                prev_col = None
                IVprev_cols = []
                for col in substr:
                    IVcols = []
                    is_first = True
                    # combine series between crossties
                    for idxs in get_series_cells(col, prev_col):
                        if not idxs:
                            # first row should always be empty since it must be
                            # crosstied
                            is_first = False
                            continue
                        elif is_first:
                            # TODO: use pvmismatch exceptions
                            raise Exception(
                                "First row and last rows must be crosstied."
                            )
                        elif len(idxs) > 1:
                            IatVrbd = np.asarray(
                                [np.interp(vrbd, v, i) for vrbd, v, i in
                                 zip(self.VRBD[idxs], self.Vcell[idxs],
                                     self.Icell[idxs])]
                            )
                            Icol, Vcol = self.pvconst.calcSeries(
                                self.Icell[idxs], self.Vcell[idxs],
                                self.Isc[idxs].mean(), IatVrbd.max()
                            )
                        else:
                            Icol, Vcol = self.Icell[idxs], self.Vcell[idxs]
                        IVcols.append([Icol, Vcol])
                    if prev_col:
                        # if circuits are same in both columns then continue
                        if not all(icol['circuit'] == jcol['circuit']
                                   for icol, jcol in zip(prev_col, col)):
                            # combine parallel circuits
                            Irows, Vrows = [], []
                            Isc_rows, Imax_rows = [], []
                            for IVprev_col, IVnext_col in zip(IVcols,
                                                              IVprev_cols):
                                Iparallel, Vparallel = zip(IVprev_col,
                                                           IVnext_col)
                                Iparallel = np.asarray(Iparallel).flatten()
                                Vparallel = np.asarray(Vparallel).flatten()
                                # interpolate Voc where Iparallel is 0
                                parallelVoc = np.interp(0.,
                                                        np.flipud(Iparallel),
                                                        np.flipud(Vparallel))
                                Irow, Vrow = self.pvconst.calcParallel(
                                    Iparallel, Vparallel, parallelVoc.mean(),
                                    Vparallel.min()
                                )
                                Irows.append(Irow)
                                Vrows.append(Vrow)
                                Isc_rows.append(np.interp(0., Vrow, Irow))
                                Imax_rows.append(Irow.max())
                            Irows, Vrows = np.asarray(Irows), np.asarray(Vrows)
                            Isc_rows = np.asarray(Isc_rows)
                            Imax_rows = np.asarray(Imax_rows)
                            Iparallel, Vparallel = self.pvconst.calcSeries(
                                Irows, Vrows, Isc_rows.mean(), Imax_rows.max()
                            )
                            IVall_cols.append([Iparallel, Vparallel])
                            # reset prev_col
                            prev_col = None
                            IVprev_cols = []
                            continue
                    # set prev_col, append IVcols and continue
                    IVprev_cols.append(IVcols)
                    prev_col = col
                if len(IVall_cols) > 1:
                    pass
                else:
                    pass
            bypassed = Vsub < self.Vbypass
            Vsub[bypassed] = self.Vbypass
            Isubstr.append(Isub)
            Vsubstr.append(Vsub)
            Isc_substr.append(np.interp(0., Vsub, Isub))
            Imax_substr.append(Isub.max())
        Isubstr, Vsubstr = np.asarray(Isubstr), np.asarray(Vsubstr)
        Isc_substr = np.asarray(Isc_substr)
        Imax_substr = np.asarray(Imax_substr)
        Imod, Vmod = self.pvconst.calcSeries(
            Isubstr, Vsubstr, Isc_substr.mean(), Imax_substr.max()
        )
        Pmod = Imod * Vmod
        return Imod, Vmod, Pmod, Vsubstr

    def plotCell(self):
        """
        Plot cell I-V curves.
        Returns cellPlot : matplotlib.pyplot figure
        """
        cellPlot = plt.figure()
        plt.subplot(2, 2, 1)
        plt.plot(self.Vcell.T, self.Icell.T)
        plt.title('Cell Reverse I-V Characteristics')
        plt.ylabel('Cell Current, I [A]')
        plt.xlim(self.VRBD.min() - 1, 0)
        plt.ylim(0, self.Isc.mean() + 10)
        plt.grid()
        plt.subplot(2, 2, 2)
        plt.plot(self.Vcell.T, self.Icell.T)
        plt.title('Cell Forward I-V Characteristics')
        plt.ylabel('Cell Current, I [A]')
        plt.xlim(0, self.Voc.max())
        plt.ylim(0, self.Isc.mean() + 1)
        plt.grid()
        plt.subplot(2, 2, 3)
        plt.plot(self.Vcell.T, self.Pcell.T)
        plt.title('Cell Reverse P-V Characteristics')
        plt.xlabel('Cell Voltage, V [V]')
        plt.ylabel('Cell Power, P [W]')
        plt.xlim(self.VRBD.min() - 1, 0)
        plt.ylim((self.Isc.mean() + 10) * (self.VRBD.min() - 1), -1)
        plt.grid()
        plt.subplot(2, 2, 4)
        plt.plot(self.Vcell.T, self.Pcell.T)
        plt.title('Cell Forward P-V Characteristics')
        plt.xlabel('Cell Voltage, V [V]')
        plt.ylabel('Cell Power, P [W]')
        plt.xlim(0, self.Voc.max())
        plt.ylim(0, (self.Isc.mean() + 1) * self.Voc.max())
        plt.grid()
        return cellPlot

    def plotMod(self):
        """
        Plot module I-V curves.
        Returns modPlot : matplotlib.pyplot figure
        """
        modPlot = plt.figure()
        plt.subplot(2, 1, 1)
        plt.plot(self.Vmod, self.Imod)
        plt.title('Module I-V Characteristics')
        plt.ylabel('Module Current, I [A]')
        plt.ylim(ymin=0)
        plt.xlim(self.Vmod.min() - 1, self.Vmod.max() + 1)
        plt.grid()
        plt.subplot(2, 1, 2)
        plt.plot(self.Vmod, self.Pmod)
        plt.title('Module P-V Characteristics')
        plt.xlabel('Module Voltage, V [V]')
        plt.ylabel('Module Power, P [W]')
        plt.ylim(ymin=0)
        plt.xlim(self.Vmod.min() - 1, self.Vmod.max() + 1)
        plt.grid()
        return modPlot
<commit_msg>combine any remaining parallel circuits<commit_after># -*- coding: utf-8 -*-
"""
This module defines the :class:`~pvmismatch.pvmismatch_lib.pvmodule.PVmodule`.
"""

import numpy as np
from copy import copy
from matplotlib import pyplot as plt
# use absolute imports instead of relative, so modules are portable
from pvmismatch.pvmismatch_lib.pvconstants import PVconstants, get_series_cells
from pvmismatch.pvmismatch_lib.pvcell import PVcell

VBYPASS = -0.5  # [V] trigger voltage of bypass diode

def standard_cellpos_pat(nrows, ncols_per_substr):
    cellpos = []
    ncols = [0, 0]
    for substr_cols in ncols_per_substr:
        ncols[0], ncols[1] = ncols[1], ncols[1] + substr_cols
        newsubstr = []
        for col in xrange(*ncols):
            newrow = []
            for row in xrange(nrows):
                idx = col * nrows
                if col % 2 == 0:
                    idx += row
                else:
                    idx += (nrows - row - 1)
                newrow.append({'circuit': 'series', 'idx': idx})
            newsubstr.append(newrow)
        cellpos.append(newsubstr)
    return cellpos

# standard cell positions presets
STD72 = standard_cellpos_pat(12, [2, 2, 2])
STD96 = standard_cellpos_pat(12, [2, 4, 2])
STD128 = standard_cellpos_pat(16, [2, 4, 2])

def crosstied_cellpos_pat(nrows_per_substrs, ncols, partial=False):
    trows = sum(nrows_per_substrs)
    cellpos = []
    nrows = [0, 0]
    for substr_row in nrows_per_substrs:
        nrows[0], nrows[1] = nrows[1], nrows[1] + substr_row
        newsubstr = []
        for col in xrange(ncols):
            newrow = []
            for row in xrange(*nrows):
                circuit = 'parallel'
                if partial and newrow:
                    circuit = 'series'
                newrow.append({'circuit': circuit, 'idx': col * trows + row})
            newsubstr.append(newrow)
        cellpos.append(newsubstr)
    return cellpos

# crosstied cell positions presets
TCT96 = crosstied_cellpos_pat([4, 4, 4], 8)
PCT96 = crosstied_cellpos_pat([4, 4, 4], 8, partial=True)

class PVmodule(object):
    """
    PVmodule - A Class for PV modules.

    :param cell_pos: cell position pattern
    :type cell_pos: dict
    :param pvcells: list of :class:`~pvmismatch.pvmismatch_lib.pvcell.PVcell`
    :type pvcells: list
    :param pvconst: An object with common parameters and constants.
    :type pvconst: :class:`~pvmismatch.pvmismatch_lib.pvconstants.PVconstants`
    :param Vbypass: bypass diode trigger voltage [V]
    :type Vbypass: float
    """
    def __init__(self, cell_pos=STD96, pvcells=None, pvconst=PVconstants(),
                 Vbypass=VBYPASS):
        # TODO: check cell position pattern
        self.cell_pos = cell_pos  #: cell position pattern dictionary
        self.numberCells = sum([len(c) for s in self.cell_pos for c in s])
        """number of cells in the module"""
        self.pvconst = pvconst  #: configuration constants
        self.Vbypass = Vbypass  #: [V] trigger voltage of bypass diode
        if pvcells is None:
            # faster to use copy instead of making each object in a for-loop
            # use copy instead of deepcopy to keey same pvconst for all objects
            # PVcell.calcCell() creates new np.ndarray if attributes change
            pvc = PVcell(pvconst=pvconst)
            pvcells = [copy(pvc) for _ in xrange(self.numberCells)]
        else:
            if len(pvcells) != self.numberCells:
                # TODO: use pvexception
                raise Exception(
                    "Number of cells doesn't match cell position pattern."
                )
        self.pvcells = pvcells  #: list of `PVcell` objects in this `PVmodule`
        self.numSubStr = len(self.cell_pos)  #: number of substrings
        self.subStrCells = [len(_) for _ in self.cell_pos]  #: cells per substr
        # initialize members so PyLint doesn't get upset
        self.Imod, self.Vmod, self.Pmod, self.Vsubstr = self.calcMod()
        # self.setSuns(Ee)

    # copy some values from cells to modules
    @property
    def Ee(self):
        return np.array([pvc.Ee.flatten() for pvc in self.pvcells])

    @property
    def Tcell(self):
        return np.array([pvc.Tcell.flatten() for pvc in self.pvcells])

    @property
    def Icell(self):
        return np.array([pvc.Icell.flatten() for pvc in self.pvcells])

    @property
    def Vcell(self):
        return np.array([pvc.Vcell.flatten() for pvc in self.pvcells])

    @property
    def Pcell(self):
        return np.array([pvc.Pcell.flatten() for pvc in self.pvcells])

    @property
    def Isc(self):
        return np.array([pvc.Isc.flatten() for pvc in self.pvcells])

    @property
    def Voc(self):
        return np.array([pvc.Voc.flatten() for pvc in self.pvcells])

    @property
    def VRBD(self):
        return np.array([pvc.VRBD.flatten() for pvc in self.pvcells])

    def setSuns(self, Ee, cells=None):
        """
        Set the irradiance in suns, Ee, on the solar cells in the module.
        Recalculates cell current (Icell [A]), voltage (Vcell [V]) and power
        (Pcell [W]) as well as module current (Imod [A]), voltage (Vmod [V])
        and power (Pmod [W]).
        Arguments
            Ee : <float> or <np.array of floats> Effective Irradiance
        Optional
            cells : <np.array of int> Cells to change
        """
        if cells is None:
            if np.isscalar(Ee):
                for pvc in self.pvcells:
                    pvc.Ee = Ee
            elif np.size(Ee) == self.numberCells:
                for pvc, Ee_idx in zip(self.pvcells, Ee):
                    pvc.Ee = Ee_idx
            else:
                raise Exception("Input irradiance value (Ee) for each cell!")
        else:
            Nsuns = np.size(cells)
            if np.isscalar(Ee):
                for cell_idx in cells:
                    self.pvcells[cell_idx].Ee = Ee
            elif np.size(Ee) == Nsuns:
                for cell_idx in cells:
                    self.pvcells[cell_idx].Ee = Ee[cell_idx]
            else:
                raise Exception("Input irradiance value (Ee) for each cell!")
        self.Imod, self.Vmod, self.Pmod, self.Vsubstr = self.calcMod()

    def calcMod(self):
        """
        Calculate module I-V curves.
        Returns (Imod, Vmod, Pmod) : tuple of numpy.ndarray of float
        """
        # iterate over substrings
        # TODO: benchmark speed difference append() vs preallocate space
        Isubstr, Vsubstr, Isc_substr, Imax_substr = [], [], [], []
        for substr in self.cell_pos:
            # check if cells are in series or any parallel circuits
            if all(r['circuit'] == 'series' for c in substr for r in c):
                idxs = [r['idx'] for c in substr for r in c]
                IatVrbd = np.asarray(
                    [np.interp(vrbd, v, i) for vrbd, v, i in
                     zip(self.VRBD[idxs], self.Vcell[idxs], self.Icell[idxs])]
                )
                Isub, Vsub = self.pvconst.calcSeries(
                    self.Icell[idxs], self.Vcell[idxs], self.Isc[idxs].mean(),
                    IatVrbd.max()
                )
            elif all(r['circuit'] == 'parallel' for c in substr for r in c):
                Irows, Vrows = [], []
                Isc_rows, Imax_rows = [], []
                for row in zip(*substr):
                    idxs = [c['idx'] for c in row]
                    Irow, Vrow = self.pvconst.calcParallel(
                        self.Icell[idxs], self.Vcell[idxs],
                        self.Voc[idxs].mean(), self.VRBD.min()
                    )
                    Irows.append(Irow)
                    Vrows.append(Vrow)
                    Isc_rows.append(np.interp(0., Vrow, Irow))
                    Imax_rows.append(Irow.max())
                Irows, Vrows = np.asarray(Irows), np.asarray(Vrows)
                Isc_rows = np.asarray(Isc_rows)
                Imax_rows = np.asarray(Imax_rows)
                Isub, Vsub = self.pvconst.calcSeries(
                    Irows, Vrows, Isc_rows.mean(), Imax_rows.max()
                )
            else:
                IVall_cols = []
                prev_col = None
                IVprev_cols = []
                for col in substr:
                    IVcols = []
                    is_first = True
                    # combine series between crossties
                    for idxs in get_series_cells(col, prev_col):
                        if not idxs:
                            # first row should always be empty since it must be
                            # crosstied
                            is_first = False
                            continue
                        elif is_first:
                            # TODO: use pvmismatch exceptions
                            raise Exception(
                                "First row and last rows must be crosstied."
                            )
                        elif len(idxs) > 1:
                            IatVrbd = np.asarray(
                                [np.interp(vrbd, v, i) for vrbd, v, i in
                                 zip(self.VRBD[idxs], self.Vcell[idxs],
                                     self.Icell[idxs])]
                            )
                            Icol, Vcol = self.pvconst.calcSeries(
                                self.Icell[idxs], self.Vcell[idxs],
                                self.Isc[idxs].mean(), IatVrbd.max()
                            )
                        else:
                            Icol, Vcol = self.Icell[idxs], self.Vcell[idxs]
                        IVcols.append([Icol, Vcol])
                    if prev_col:
                        # if circuits are same in both columns then continue
                        if not all(icol['circuit'] == jcol['circuit']
                                   for icol, jcol in zip(prev_col, col)):
                            # combine parallel circuits
                            Irows, Vrows = [], []
                            Isc_rows, Imax_rows = [], []
                            for IVcols in zip(*IVprev_cols):
                                Iparallel, Vparallel = zip(*IVcols)
                                Iparallel = np.asarray(Iparallel).flatten()
                                Vparallel = np.asarray(Vparallel).flatten()
                                # interpolate Voc where Iparallel is 0
                                parallelVoc = np.interp(0.,
                                                        np.flipud(Iparallel),
                                                        np.flipud(Vparallel))
                                Irow, Vrow = self.pvconst.calcParallel(
                                    Iparallel, Vparallel, parallelVoc.mean(),
                                    Vparallel.min()
                                )
                                Irows.append(Irow)
                                Vrows.append(Vrow)
                                Isc_rows.append(np.interp(0., Vrow, Irow))
                                Imax_rows.append(Irow.max())
                            Irows, Vrows = np.asarray(Irows), np.asarray(Vrows)
                            Isc_rows = np.asarray(Isc_rows)
                            Imax_rows = np.asarray(Imax_rows)
                            Iparallel, Vparallel = self.pvconst.calcSeries(
                                Irows, Vrows, Isc_rows.mean(), Imax_rows.max()
                            )
                            IVall_cols.append([Iparallel, Vparallel])
                            # reset prev_col
                            prev_col = None
                            IVprev_cols = []
                            continue
                    # set prev_col, append IVcols and continue
                    IVprev_cols.append(IVcols)
                    prev_col = col
                # combine any remaining parallel circuits in substring
                if len(IVall_cols) == 1:
                    Isub, Vsub = IVall_cols[0]
                else:
                    Iparallel, Vparallel = zip(*IVall_cols)
                    Iparallel = np.asarray(Iparallel).flatten()
                    Vparallel = np.asarray(Vparallel).flatten()
                    # interpolate Voc where Iparallel is 0
                    parallelVoc = np.interp(0.,
                                            np.flipud(Iparallel),
                                            np.flipud(Vparallel))
                    Isub, Vsub = self.pvconst.calcParallel(
                                    Iparallel, Vparallel, parallelVoc.mean(),
                                    Vparallel.min()
                                )
            bypassed = Vsub < self.Vbypass
            Vsub[bypassed] = self.Vbypass
            Isubstr.append(Isub)
            Vsubstr.append(Vsub)
            Isc_substr.append(np.interp(0., Vsub, Isub))
            Imax_substr.append(Isub.max())
        Isubstr, Vsubstr = np.asarray(Isubstr), np.asarray(Vsubstr)
        Isc_substr = np.asarray(Isc_substr)
        Imax_substr = np.asarray(Imax_substr)
        Imod, Vmod = self.pvconst.calcSeries(
            Isubstr, Vsubstr, Isc_substr.mean(), Imax_substr.max()
        )
        Pmod = Imod * Vmod
        return Imod, Vmod, Pmod, Vsubstr

    def plotCell(self):
        """
        Plot cell I-V curves.
        Returns cellPlot : matplotlib.pyplot figure
        """
        cellPlot = plt.figure()
        plt.subplot(2, 2, 1)
        plt.plot(self.Vcell.T, self.Icell.T)
        plt.title('Cell Reverse I-V Characteristics')
        plt.ylabel('Cell Current, I [A]')
        plt.xlim(self.VRBD.min() - 1, 0)
        plt.ylim(0, self.Isc.mean() + 10)
        plt.grid()
        plt.subplot(2, 2, 2)
        plt.plot(self.Vcell.T, self.Icell.T)
        plt.title('Cell Forward I-V Characteristics')
        plt.ylabel('Cell Current, I [A]')
        plt.xlim(0, self.Voc.max())
        plt.ylim(0, self.Isc.mean() + 1)
        plt.grid()
        plt.subplot(2, 2, 3)
        plt.plot(self.Vcell.T, self.Pcell.T)
        plt.title('Cell Reverse P-V Characteristics')
        plt.xlabel('Cell Voltage, V [V]')
        plt.ylabel('Cell Power, P [W]')
        plt.xlim(self.VRBD.min() - 1, 0)
        plt.ylim((self.Isc.mean() + 10) * (self.VRBD.min() - 1), -1)
        plt.grid()
        plt.subplot(2, 2, 4)
        plt.plot(self.Vcell.T, self.Pcell.T)
        plt.title('Cell Forward P-V Characteristics')
        plt.xlabel('Cell Voltage, V [V]')
        plt.ylabel('Cell Power, P [W]')
        plt.xlim(0, self.Voc.max())
        plt.ylim(0, (self.Isc.mean() + 1) * self.Voc.max())
        plt.grid()
        return cellPlot

    def plotMod(self):
        """
        Plot module I-V curves.
        Returns modPlot : matplotlib.pyplot figure
        """
        modPlot = plt.figure()
        plt.subplot(2, 1, 1)
        plt.plot(self.Vmod, self.Imod)
        plt.title('Module I-V Characteristics')
        plt.ylabel('Module Current, I [A]')
        plt.ylim(ymin=0)
        plt.xlim(self.Vmod.min() - 1, self.Vmod.max() + 1)
        plt.grid()
        plt.subplot(2, 1, 2)
        plt.plot(self.Vmod, self.Pmod)
        plt.title('Module P-V Characteristics')
        plt.xlabel('Module Voltage, V [V]')
        plt.ylabel('Module Power, P [W]')
        plt.ylim(ymin=0)
        plt.xlim(self.Vmod.min() - 1, self.Vmod.max() + 1)
        plt.grid()
        return modPlot
<|endoftext|>


<commit_before>"""
===========
Middlewares
===========

Collection of useful middlewares for aiohttp applications.

"""

__version__ = "1.1.0"

from .constants import IDEMPOTENT_METHODS, NON_IDEMPOTENT_METHODS
from .cors import cors_middleware
from .error import (
    default_error_handler,
    error_context,
    error_middleware,
    get_error_response,
)
from .https import https_middleware
from .shield import shield_middleware
from .timeout import timeout_middleware
from .utils import match_path

# Make flake8 happy
(
    cors_middleware,
    default_error_handler,
    error_context,
    error_middleware,
    get_error_response,
    https_middleware,
    IDEMPOTENT_METHODS,
    match_path,
    NON_IDEMPOTENT_METHODS,
    shield_middleware,
    timeout_middleware,
)
<commit_msg>chore: Update package version<commit_after>"""
===========
Middlewares
===========

Collection of useful middlewares for aiohttp applications.

"""

__author__ = "Igor Davydenko"
__license__ = "BSD-3-Clause"
__version__ = "1.2.0a0"

from .constants import IDEMPOTENT_METHODS, NON_IDEMPOTENT_METHODS
from .cors import cors_middleware
from .error import (
    default_error_handler,
    error_context,
    error_middleware,
    get_error_response,
)
from .https import https_middleware
from .shield import shield_middleware
from .timeout import timeout_middleware
from .utils import match_path

# Make flake8 happy
(
    cors_middleware,
    default_error_handler,
    error_context,
    error_middleware,
    get_error_response,
    https_middleware,
    IDEMPOTENT_METHODS,
    match_path,
    NON_IDEMPOTENT_METHODS,
    shield_middleware,
    timeout_middleware,
)
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2012-2021 GEM Foundation
#
# OpenQuake is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OpenQuake is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with OpenQuake. If not, see <http://www.gnu.org/licenses/>.

import ast
import sys
import copy
import operator
import itertools
from contextlib import contextmanager
import numpy
from scipy.spatial import cKDTree

from openquake.baselib.python3compat import raise_
from openquake.hazardlib import site, mfd
from openquake.hazardlib.geo.utils import (
    KM_TO_DEGREES, angular_distance, fix_lon, get_bounding_box,
    get_longitudinal_extent, BBoxError, spherical_to_cartesian)

U32 = numpy.uint32
MAX_DISTANCE = 2000  # km, ultra big distance used if there is no filter
trt_smr = operator.attrgetter('trt_smr')

@contextmanager
def context(src):
    """
    Used to add the source_id to the error message. To be used as

    with context(src):
        operation_with(src)

    Typically the operation is filtering a source, that can fail for
    tricky geometries.
    """
    try:
        yield
    except Exception:
        etype, err, tb = sys.exc_info()
        msg = 'An error occurred with source id=%s. Error: %s'
        msg %= (src.source_id, err)
        raise_(etype, msg, tb)

def getdefault(dic_with_default, key):
    """
    :param dic_with_default: a dictionary with a 'default' key
    :param key: a key that may be present in the dictionary or not
    :returns: the value associated to the key, or to 'default'
    """
    try:
        return dic_with_default[key]
    except KeyError:
        return dic_with_default['default']

def unique_sorted(items):
    """
    Check that the items are unique and sorted
    """
    if len(set(items)) < len(items):
        raise ValueError('Found duplicates in %s' % items)
    elif items != sorted(items):
        raise ValueError('%s is not ordered' % items)
    return items

# used for the maximum distance parameter in the job.ini file
def floatdict(value):
    """
    :param value:
        input string corresponding to a literal Python number or dictionary
    :returns:
        a Python dictionary key -> number

    >>> floatdict("200")
    {'default': 200}

    >>> floatdict("{'active shallow crust': 250., 'default': 200}")
    {'active shallow crust': 250.0, 'default': 200}
    """
    value = ast.literal_eval(value)
    if isinstance(value, (int, float, list)):
        return {'default': value}
    return value

class MagDepDistance(dict):
    """
    A dictionary trt -> [(mag, dist), ...]
    """
    @classmethod
    def new(cls, value):
        """
        :param value: string to be converted
        :returns: MagDepDistance dictionary

        >>> md = MagDepDistance.new('50')
        >>> md
        {'default': [(1.0, 50), (10.0, 50)]}
        >>> md.max()
        {'default': 50}
        >>> md.interp(dict(default=[5.0, 5.1, 5.2])); md.ddic
        {'default': {'5.00': 50.0, '5.10': 50.0, '5.20': 50.0}}
        """
        items_by_trt = floatdict(value)
        self = cls()
        for trt, items in items_by_trt.items():
            if isinstance(items, list):
                self[trt] = unique_sorted([tuple(it) for it in items])
                for mag, dist in self[trt]:
                    if mag < 1 or mag > 10:
                        raise ValueError('Invalid magnitude %s' % mag)
            else:  # assume scalar distance
                assert items >= 0, items
                self[trt] = [(1., items), (10., items)]
        return self

    def interp(self, mags_by_trt):
        """
        :param mags_by_trt: a dictionary trt -> magnitudes as strings
        :returns: a dictionary trt->mag->dist
        """
        ddic = {}
        for trt, mags in mags_by_trt.items():
            xs, ys = zip(*getdefault(self, trt))
            if len(mags) == 1:
                ms = [numpy.float64(mags)]
            else:
                ms = numpy.float64(mags)
            dists = numpy.interp(ms, xs, ys)
            ddic[trt] = {'%.2f' % mag: dist for mag, dist in zip(ms, dists)}
        self.ddic = ddic

    def __call__(self, trt, mag=None):
        if not self:
            return MAX_DISTANCE
        elif mag is None:
            return getdefault(self, trt)[-1][1]
        elif hasattr(self, 'ddic'):
            return self.ddic[trt]['%.2f' % mag]
        else:
            xs, ys = zip(*getdefault(self, trt))
            return numpy.interp(mag, xs, ys)

    def max(self):
        """
        :returns: a dictionary trt -> maxdist
        """
        return {trt: self[trt][-1][1] for trt in self}

    def get_bounding_box(self, lon, lat, trt=None, mag=None):
        """
        Build a bounding box around the given lon, lat by computing the
        maximum_distance at the given tectonic region type and magnitude.

        :param lon: longitude
        :param lat: latitude
        :param trt: tectonic region type, possibly None
        :param mag: magnitude, possibly None
        :returns: min_lon, min_lat, max_lon, max_lat
        """
        if trt is None:  # take the greatest integration distance
            maxdist = max(self(trt, mag) for trt in self)
        else:  # get the integration distance for the given TRT
            maxdist = self(trt, mag)
        a1 = min(maxdist * KM_TO_DEGREES, 90)
        a2 = min(angular_distance(maxdist, lat), 180)
        return lon - a2, lat - a1, lon + a2, lat + a1

    def get_dist_bins(self, trt, nbins=51):
        """
        :returns: an array of distance bins, from 10m to maxdist
        """
        return .01 + numpy.arange(nbins) * self(trt) / (nbins - 1)

def split_source(src):
    """
    :param src: a splittable (or not splittable) source
    :returns: the underlying sources (or the source itself)
    """
    from openquake.hazardlib.source import splittable  # avoid circular import
    if not splittable(src):
        return [src]
    mag_a, mag_b = src.get_min_max_mag()
    min_mag = src.min_mag
    if mag_b < min_mag:  # discard the source completely
        return [src]
    if min_mag:
        splits = []
        for s in src:
            s.min_mag = min_mag
            mag_a, mag_b = s.get_min_max_mag()
            if mag_b >= min_mag:
                splits.append(s)
    else:
        splits = list(src)
    has_samples = hasattr(src, 'samples')
    has_scaling_rate = hasattr(src, 'scaling_rate')
    grp_id = getattr(src, 'grp_id', 0)  # 0 in hazardlib
    if len(splits) > 1:
        for i, split in enumerate(splits):
            split.source_id = '%s:%s' % (src.source_id, i)
            split.trt_smr = src.trt_smr
            split.grp_id = grp_id
            split.id = src.id
            if has_samples:
                split.samples = src.samples
            if has_scaling_rate:
                s.scaling_rate = src.scaling_rate
    elif splits:  # single source
        [s] = splits
        s.source_id = src.source_id
        s.trt_smr = src.trt_smr
        s.grp_id = grp_id
        s.id = src.id
        if has_samples:
            s.samples = src.samples
        if has_scaling_rate:
            s.scaling_rate = src.scaling_rate
    for split in splits:
        if not split.num_ruptures:
            split.num_ruptures = split.count_ruptures()
    return splits

class SourceFilter(object):
    """
    Filter objects have a .filter method yielding filtered sources
    and the IDs of the sites within the given maximum distance.
    Filter the sources by using `self.sitecol.within_bbox` which is
    based on numpy.
    """
    def __init__(self, sitecol, integration_distance):
        if sitecol is None:
            integration_distance = {}
        self.sitecol = sitecol
        self.integration_distance = (
            integration_distance
            if isinstance(integration_distance, MagDepDistance)
            else MagDepDistance(integration_distance))
        self.slc = slice(None)

    def split_in_tiles(self, hint):
        """
        Split the SourceFilter by splitting the site collection in tiles
        """
        if hint == 1:
            return [self]
        out = []
        for tile in self.sitecol.split_in_tiles(hint):
            sf = self.__class__(tile, self.integration_distance)
            sf.slc = slice(tile.sids[0], tile.sids[-1] + 1)
            out.append(sf)
        return out

    # not used right now
    def reduce(self, factor=100):
        """
        Reduce the SourceFilter to a subset of sites
        """
        idxs = numpy.arange(0, len(self.sitecol), factor)
        sc = object.__new__(site.SiteCollection)
        sc.array = self.sitecol[idxs]
        sc.complete = self.sitecol.complete
        return self.__class__(sc, self.integration_distance)

    def get_enlarged_box(self, src, maxdist=None):
        """
        Get the enlarged bounding box of a source.

        :param src: a source object
        :param maxdist: a scalar maximum distance (or None)
        :returns: a bounding box (min_lon, min_lat, max_lon, max_lat)
        """
        if maxdist is None:
            maxdist = self.integration_distance(src.tectonic_region_type)
        try:
            bbox = get_bounding_box(src, maxdist)
        except Exception as exc:
            raise exc.__class__('source %s: %s' % (src.source_id, exc))
        return (fix_lon(bbox[0]), bbox[1], fix_lon(bbox[2]), bbox[3])

    def get_rectangle(self, src):
        """
        :param src: a source object
        :returns: ((min_lon, min_lat), width, height), useful for plotting
        """
        min_lon, min_lat, max_lon, max_lat = self.get_enlarged_box(src)
        return (min_lon, min_lat), (max_lon - min_lon) % 360, max_lat - min_lat

    def get_close_sites(self, source):
        """
        Returns the sites within the integration distance from the source,
        or None.
        """
        source_sites = list(self.filter([source]))
        if source_sites:
            return source_sites[0][1]

    def split(self, sources):
        """
        :yields: pairs (split, sites)
        """
        for src, _sites in self.filter(sources):
            for s in split_source(src):
                sites = self.get_close_sites(s)
                if sites is not None:
                    yield s, sites

    def split_less(self, sources):
        """
        :yields: pairs (split, sites)
        """
        for src, _sites in self.filter(sources):
            if src.__class__.__name__.startswith(('Multi', 'Collapsed')):
                # do not split
                yield src, _sites
            elif hasattr(src, 'get_annual_occurrence_rates'):
                for mag, rate in src.get_annual_occurrence_rates():
                    new = copy.copy(src)
                    new.mfd = mfd.ArbitraryMFD([mag], [rate])
                    new.num_ruptures = new.count_ruptures()
                    sites = self.get_close_sites(new)
                    if sites is not None:
                        yield new, sites
            else:  # nonparametric source
                # data is a list of pairs (rup, pmf)
                for mag, group in itertools.groupby(
                        src.data, lambda pair: pair[0].mag):
                    new = src.__class__(src.source_id, src.name,
                                        src.tectonic_region_type, list(group))
                    vars(new).update(vars(src))
                    sites = self.get_close_sites(new)
                    if sites is not None:
                        yield new, sites

    # used in source and rupture prefiltering: it should not discard too much
    def close_sids(self, src_or_rec, trt=None, maxdist=None):
        """
        :param src_or_rec: a source or a rupture record
        :param trt: passed only if src_or_rec is a rupture record
        :returns:
           the site indices within the maximum_distance of the hypocenter,
           plus the maximum size of the bounding box
        """
        if self.sitecol is None:
            return []
        elif not self.integration_distance:  # do not filter
            return self.sitecol.sids
        if trt:  # rupture, called by GmfGetter.gen_computers
            dlon = get_longitudinal_extent(
                src_or_rec['minlon'], src_or_rec['maxlon']) / 2.
            dlat = (src_or_rec['maxlat'] - src_or_rec['minlat']) / 2.
            lon, lat, dep = src_or_rec['hypo']
            dist = self.integration_distance(trt) + numpy.sqrt(
                dlon**2 + dlat**2) / KM_TO_DEGREES
            dist += 10  # added 10 km of buffer to guard against numeric errors
            # the test most sensitive to the buffer effect is in oq-risk-tests,
            # case_ucerf/job_eb.ini; without buffer, sites can be discarded
            # even if within the maximum_distance
            return self._close_sids(lon, lat, dep, dist)
        else:  # source
            trt = src_or_rec.tectonic_region_type
            try:
                bbox = self.get_enlarged_box(src_or_rec, maxdist)
            except BBoxError:  # do not filter
                return self.sitecol.sids
            return self.sitecol.within_bbox(bbox)

    def _close_sids(self, lon, lat, dep, dist):
        if not hasattr(self, 'kdt'):
            self.kdt = cKDTree(self.sitecol.xyz)
        xyz = spherical_to_cartesian(lon, lat, dep)
        sids = U32(self.kdt.query_ball_point(xyz, dist, eps=.001))
        sids.sort()
        return sids

    def filter(self, sources):
        """
        :param sources: a sequence of sources
        :yields: pairs (sources, sites)
        """
        if self.sitecol is None:  # nofilter
            for src in sources:
                yield src, None
            return
        for src in sources:
            sids = self.close_sids(src)
            if len(sids):
                yield src, self.sitecol.filtered(sids)

    def __getitem__(self, slc):
        if slc.start is None and slc.stop is None:
            return self
        sitecol = object.__new__(self.sitecol.__class__)
        sitecol.array = self.sitecol[slc]
        sitecol.complete = self.sitecol.complete
        return self.__class__(sitecol, self.integration_distance)

nofilter = SourceFilter(None, {})
<commit_msg>Removed unused method<commit_after># -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2012-2021 GEM Foundation
#
# OpenQuake is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OpenQuake is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with OpenQuake. If not, see <http://www.gnu.org/licenses/>.

import ast
import sys
import copy
import operator
import itertools
from contextlib import contextmanager
import numpy
from scipy.spatial import cKDTree

from openquake.baselib.python3compat import raise_
from openquake.hazardlib import site, mfd
from openquake.hazardlib.geo.utils import (
    KM_TO_DEGREES, angular_distance, fix_lon, get_bounding_box,
    get_longitudinal_extent, BBoxError, spherical_to_cartesian)

U32 = numpy.uint32
MAX_DISTANCE = 2000  # km, ultra big distance used if there is no filter
trt_smr = operator.attrgetter('trt_smr')

@contextmanager
def context(src):
    """
    Used to add the source_id to the error message. To be used as

    with context(src):
        operation_with(src)

    Typically the operation is filtering a source, that can fail for
    tricky geometries.
    """
    try:
        yield
    except Exception:
        etype, err, tb = sys.exc_info()
        msg = 'An error occurred with source id=%s. Error: %s'
        msg %= (src.source_id, err)
        raise_(etype, msg, tb)

def getdefault(dic_with_default, key):
    """
    :param dic_with_default: a dictionary with a 'default' key
    :param key: a key that may be present in the dictionary or not
    :returns: the value associated to the key, or to 'default'
    """
    try:
        return dic_with_default[key]
    except KeyError:
        return dic_with_default['default']

def unique_sorted(items):
    """
    Check that the items are unique and sorted
    """
    if len(set(items)) < len(items):
        raise ValueError('Found duplicates in %s' % items)
    elif items != sorted(items):
        raise ValueError('%s is not ordered' % items)
    return items

# used for the maximum distance parameter in the job.ini file
def floatdict(value):
    """
    :param value:
        input string corresponding to a literal Python number or dictionary
    :returns:
        a Python dictionary key -> number

    >>> floatdict("200")
    {'default': 200}

    >>> floatdict("{'active shallow crust': 250., 'default': 200}")
    {'active shallow crust': 250.0, 'default': 200}
    """
    value = ast.literal_eval(value)
    if isinstance(value, (int, float, list)):
        return {'default': value}
    return value

class MagDepDistance(dict):
    """
    A dictionary trt -> [(mag, dist), ...]
    """
    @classmethod
    def new(cls, value):
        """
        :param value: string to be converted
        :returns: MagDepDistance dictionary

        >>> md = MagDepDistance.new('50')
        >>> md
        {'default': [(1.0, 50), (10.0, 50)]}
        >>> md.max()
        {'default': 50}
        >>> md.interp(dict(default=[5.0, 5.1, 5.2])); md.ddic
        {'default': {'5.00': 50.0, '5.10': 50.0, '5.20': 50.0}}
        """
        items_by_trt = floatdict(value)
        self = cls()
        for trt, items in items_by_trt.items():
            if isinstance(items, list):
                self[trt] = unique_sorted([tuple(it) for it in items])
                for mag, dist in self[trt]:
                    if mag < 1 or mag > 10:
                        raise ValueError('Invalid magnitude %s' % mag)
            else:  # assume scalar distance
                assert items >= 0, items
                self[trt] = [(1., items), (10., items)]
        return self

    def interp(self, mags_by_trt):
        """
        :param mags_by_trt: a dictionary trt -> magnitudes as strings
        :returns: a dictionary trt->mag->dist
        """
        ddic = {}
        for trt, mags in mags_by_trt.items():
            xs, ys = zip(*getdefault(self, trt))
            if len(mags) == 1:
                ms = [numpy.float64(mags)]
            else:
                ms = numpy.float64(mags)
            dists = numpy.interp(ms, xs, ys)
            ddic[trt] = {'%.2f' % mag: dist for mag, dist in zip(ms, dists)}
        self.ddic = ddic

    def __call__(self, trt, mag=None):
        if not self:
            return MAX_DISTANCE
        elif mag is None:
            return getdefault(self, trt)[-1][1]
        elif hasattr(self, 'ddic'):
            return self.ddic[trt]['%.2f' % mag]
        else:
            xs, ys = zip(*getdefault(self, trt))
            return numpy.interp(mag, xs, ys)

    def max(self):
        """
        :returns: a dictionary trt -> maxdist
        """
        return {trt: self[trt][-1][1] for trt in self}

    def get_bounding_box(self, lon, lat, trt=None, mag=None):
        """
        Build a bounding box around the given lon, lat by computing the
        maximum_distance at the given tectonic region type and magnitude.

        :param lon: longitude
        :param lat: latitude
        :param trt: tectonic region type, possibly None
        :param mag: magnitude, possibly None
        :returns: min_lon, min_lat, max_lon, max_lat
        """
        if trt is None:  # take the greatest integration distance
            maxdist = max(self(trt, mag) for trt in self)
        else:  # get the integration distance for the given TRT
            maxdist = self(trt, mag)
        a1 = min(maxdist * KM_TO_DEGREES, 90)
        a2 = min(angular_distance(maxdist, lat), 180)
        return lon - a2, lat - a1, lon + a2, lat + a1

    def get_dist_bins(self, trt, nbins=51):
        """
        :returns: an array of distance bins, from 10m to maxdist
        """
        return .01 + numpy.arange(nbins) * self(trt) / (nbins - 1)

def split_source(src):
    """
    :param src: a splittable (or not splittable) source
    :returns: the underlying sources (or the source itself)
    """
    from openquake.hazardlib.source import splittable  # avoid circular import
    if not splittable(src):
        return [src]
    mag_a, mag_b = src.get_min_max_mag()
    min_mag = src.min_mag
    if mag_b < min_mag:  # discard the source completely
        return [src]
    if min_mag:
        splits = []
        for s in src:
            s.min_mag = min_mag
            mag_a, mag_b = s.get_min_max_mag()
            if mag_b >= min_mag:
                splits.append(s)
    else:
        splits = list(src)
    has_samples = hasattr(src, 'samples')
    has_scaling_rate = hasattr(src, 'scaling_rate')
    grp_id = getattr(src, 'grp_id', 0)  # 0 in hazardlib
    if len(splits) > 1:
        for i, split in enumerate(splits):
            split.source_id = '%s:%s' % (src.source_id, i)
            split.trt_smr = src.trt_smr
            split.grp_id = grp_id
            split.id = src.id
            if has_samples:
                split.samples = src.samples
            if has_scaling_rate:
                s.scaling_rate = src.scaling_rate
    elif splits:  # single source
        [s] = splits
        s.source_id = src.source_id
        s.trt_smr = src.trt_smr
        s.grp_id = grp_id
        s.id = src.id
        if has_samples:
            s.samples = src.samples
        if has_scaling_rate:
            s.scaling_rate = src.scaling_rate
    for split in splits:
        if not split.num_ruptures:
            split.num_ruptures = split.count_ruptures()
    return splits

class SourceFilter(object):
    """
    Filter objects have a .filter method yielding filtered sources
    and the IDs of the sites within the given maximum distance.
    Filter the sources by using `self.sitecol.within_bbox` which is
    based on numpy.
    """
    def __init__(self, sitecol, integration_distance):
        if sitecol is None:
            integration_distance = {}
        self.sitecol = sitecol
        self.integration_distance = (
            integration_distance
            if isinstance(integration_distance, MagDepDistance)
            else MagDepDistance(integration_distance))
        self.slc = slice(None)

    # not used right now
    def reduce(self, factor=100):
        """
        Reduce the SourceFilter to a subset of sites
        """
        idxs = numpy.arange(0, len(self.sitecol), factor)
        sc = object.__new__(site.SiteCollection)
        sc.array = self.sitecol[idxs]
        sc.complete = self.sitecol.complete
        return self.__class__(sc, self.integration_distance)

    def get_enlarged_box(self, src, maxdist=None):
        """
        Get the enlarged bounding box of a source.

        :param src: a source object
        :param maxdist: a scalar maximum distance (or None)
        :returns: a bounding box (min_lon, min_lat, max_lon, max_lat)
        """
        if maxdist is None:
            maxdist = self.integration_distance(src.tectonic_region_type)
        try:
            bbox = get_bounding_box(src, maxdist)
        except Exception as exc:
            raise exc.__class__('source %s: %s' % (src.source_id, exc))
        return (fix_lon(bbox[0]), bbox[1], fix_lon(bbox[2]), bbox[3])

    def get_rectangle(self, src):
        """
        :param src: a source object
        :returns: ((min_lon, min_lat), width, height), useful for plotting
        """
        min_lon, min_lat, max_lon, max_lat = self.get_enlarged_box(src)
        return (min_lon, min_lat), (max_lon - min_lon) % 360, max_lat - min_lat

    def get_close_sites(self, source):
        """
        Returns the sites within the integration distance from the source,
        or None.
        """
        source_sites = list(self.filter([source]))
        if source_sites:
            return source_sites[0][1]

    def split(self, sources):
        """
        :yields: pairs (split, sites)
        """
        for src, _sites in self.filter(sources):
            for s in split_source(src):
                sites = self.get_close_sites(s)
                if sites is not None:
                    yield s, sites

    def split_less(self, sources):
        """
        :yields: pairs (split, sites)
        """
        for src, _sites in self.filter(sources):
            if src.__class__.__name__.startswith(('Multi', 'Collapsed')):
                # do not split
                yield src, _sites
            elif hasattr(src, 'get_annual_occurrence_rates'):
                for mag, rate in src.get_annual_occurrence_rates():
                    new = copy.copy(src)
                    new.mfd = mfd.ArbitraryMFD([mag], [rate])
                    new.num_ruptures = new.count_ruptures()
                    sites = self.get_close_sites(new)
                    if sites is not None:
                        yield new, sites
            else:  # nonparametric source
                # data is a list of pairs (rup, pmf)
                for mag, group in itertools.groupby(
                        src.data, lambda pair: pair[0].mag):
                    new = src.__class__(src.source_id, src.name,
                                        src.tectonic_region_type, list(group))
                    vars(new).update(vars(src))
                    sites = self.get_close_sites(new)
                    if sites is not None:
                        yield new, sites

    # used in source and rupture prefiltering: it should not discard too much
    def close_sids(self, src_or_rec, trt=None, maxdist=None):
        """
        :param src_or_rec: a source or a rupture record
        :param trt: passed only if src_or_rec is a rupture record
        :returns:
           the site indices within the maximum_distance of the hypocenter,
           plus the maximum size of the bounding box
        """
        if self.sitecol is None:
            return []
        elif not self.integration_distance:  # do not filter
            return self.sitecol.sids
        if trt:  # rupture, called by GmfGetter.gen_computers
            dlon = get_longitudinal_extent(
                src_or_rec['minlon'], src_or_rec['maxlon']) / 2.
            dlat = (src_or_rec['maxlat'] - src_or_rec['minlat']) / 2.
            lon, lat, dep = src_or_rec['hypo']
            dist = self.integration_distance(trt) + numpy.sqrt(
                dlon**2 + dlat**2) / KM_TO_DEGREES
            dist += 10  # added 10 km of buffer to guard against numeric errors
            # the test most sensitive to the buffer effect is in oq-risk-tests,
            # case_ucerf/job_eb.ini; without buffer, sites can be discarded
            # even if within the maximum_distance
            return self._close_sids(lon, lat, dep, dist)
        else:  # source
            trt = src_or_rec.tectonic_region_type
            try:
                bbox = self.get_enlarged_box(src_or_rec, maxdist)
            except BBoxError:  # do not filter
                return self.sitecol.sids
            return self.sitecol.within_bbox(bbox)

    def _close_sids(self, lon, lat, dep, dist):
        if not hasattr(self, 'kdt'):
            self.kdt = cKDTree(self.sitecol.xyz)
        xyz = spherical_to_cartesian(lon, lat, dep)
        sids = U32(self.kdt.query_ball_point(xyz, dist, eps=.001))
        sids.sort()
        return sids

    def filter(self, sources):
        """
        :param sources: a sequence of sources
        :yields: pairs (sources, sites)
        """
        if self.sitecol is None:  # nofilter
            for src in sources:
                yield src, None
            return
        for src in sources:
            sids = self.close_sids(src)
            if len(sids):
                yield src, self.sitecol.filtered(sids)

    def __getitem__(self, slc):
        if slc.start is None and slc.stop is None:
            return self
        sitecol = object.__new__(self.sitecol.__class__)
        sitecol.array = self.sitecol[slc]
        sitecol.complete = self.sitecol.complete
        return self.__class__(sitecol, self.integration_distance)

nofilter = SourceFilter(None, {})
<|endoftext|>


<commit_before># Copyright 2020 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#
"""Unit tests for owners_checks.py."""
import pathlib
import tempfile
from typing import Iterable, Sequence, Tuple
import unittest
from unittest import mock
from pw_presubmit import owners_checks

# ===== Test data =====

bad_duplicate = """\
# Should raise OwnersDuplicateError.
set noparent

file:/foo/OWNERZ
file:../OWNERS
file:../OWNERS
test1@example.com
#Test 2 comment
test2@example.com
"""

bad_duplicate_user = """\
# Should raise OwnersDuplicateError.
set noparent

file:/foo/OWNERZ
file:../OWNERS

*
test1@example.com
#Test 2 comment
test2@example.com
                        test1@example.com
"""

bad_duplicate_wildcard = """\
# Should raise OwnersDuplicateError.
set noparent
*
file:/foo/OWNERZ
file:../OWNERS
test1@example.com
#Test 2 comment
test2@example.com
*
"""

bad_email = """\
# Should raise OwnersInvalidLineError.
set noparent
*
file:/foo/OWNERZ
file:../OWNERS
test1example.com
#Test 2 comment
test2@example.com
*
"""
bad_grant_combo = """\
# Should raise OwnersUserGrantError.

file:/foo/OWNERZ
file:../OWNERS

test1@example.com
#Test noparent comment
set noparent
test2@example.com

*
"""

bad_ordering1 = """\
# Tests formatter reorders groupings of lines into the right order.
file:/foo/OWNERZ
file:bar/OWNERZ

test1@example.com
#Test noparent comment
set noparent
test2@example.com
"""

bad_ordering1_fixed = """\
#Test noparent comment
set noparent

# Tests formatter reorders groupings of lines into the right order.
file:/foo/OWNERZ
file:bar/OWNERZ

test1@example.com
test2@example.com
"""

bad_prohibited1 = """\
# Should raise OwnersProhibitedError.
set noparent

file:/foo/OWNERZ
file:../OWNERS

test1@example.com
#Test 2 comment
test2@example.com

include file1.txt

per-file foo.txt=test3@example.com
"""

bad_moving_comments = """\
# Test comments move with the rule that follows them.
test2@example.com
test1@example.com

# foo comment
file:/foo/OWNERZ
# .. comment
file:../OWNERS

set noparent
"""
bad_moving_comments_fixed = """\
set noparent

# .. comment
file:../OWNERS
# foo comment
file:/foo/OWNERZ

test1@example.com
# Test comments move with the rule that follows them.
test2@example.com
"""

bad_whitespace = """\
 set   noparent

file:/foo/OWNERZ

  file:../OWNERS

test1@example.com
#Test 2    comment
    test2@example.com

"""

bad_whitespace_fixed = """\
set noparent

file:../OWNERS
file:/foo/OWNERZ

test1@example.com
#Test 2    comment
test2@example.com
"""

no_dependencies = """\
# Test no imports are found when there are none.
set noparent

test1@example.com
#Test 2 comment
test2@example.com
"""

has_dependencies_file = """\
# Test if owners checks examine file: imports.
set noparent

file:foo_owners
file:bar_owners

test1@example.com
#Test 2 comment
test2@example.com
"""

has_dependencies_perfile = """\
# Test if owners checks examine per-file imports.
set noparent

test1@example.com
#Test 2 comment
test2@example.com

per-file *.txt=file:foo_owners
per-file *.md=example.google.com
"""

has_dependencies_include = """\
# Test if owners checks examine per-file imports.
set noparent

test1@example.com
#Test 2 comment
test2@example.com

per-file *.txt=file:foo_owners
per-file *.md=example.google.com
"""

dependencies_paths_relative = """\
set noparent

include foo/bar/../include_owners

file:foo/bar/../file_owners

*

per-file *.txt=file:foo/bar/../perfile_owners
"""

dependencies_paths_absolute = """\
set noparent

include /test/include_owners

file:/test/file_owners

*

per-file *.txt=file:/test/perfile_owners
"""

good1 = """\
# Checks should fine this formatted correctly
set noparent

include good1_include

file:good1_file

test1@example.com
#Test 2 comment
test2@example.com #{LAST_RESORT_SUGGESTION}
# LAST LINE
"""

good1_include = """\
test1@example.comom
"""

good1_file = """\
# Checks should fine this formatted correctly.
test1@example.com
"""

foo_owners = """\
test1@example.com
"""

bar_owners = """\
test1@example.com
"""

BAD_TEST_FILES = (
    ("bad_duplicate", owners_checks.OwnersDuplicateError),
    ("bad_duplicate_user", owners_checks.OwnersDuplicateError),
    ("bad_duplicate_wildcard", owners_checks.OwnersDuplicateError),
    ("bad_email", owners_checks.OwnersInvalidLineError),
    ("bad_grant_combo", owners_checks.OwnersUserGrantError),
    ("bad_ordering1", owners_checks.OwnersStyleError),
    ("bad_prohibited1", owners_checks.OwnersProhibitedError),
)

STYLING_CHECKS = (
    ("bad_moving_comments", "bad_moving_comments_fixed"),
    ("bad_ordering1", "bad_ordering1_fixed"),
    ("bad_whitespace", "bad_whitespace_fixed"),
)

DEPENDENCY_TEST_CASES: Iterable[Tuple[str, Iterable[str]]] = (
    ("no_dependencies", tuple()),
    ("has_dependencies_file", ("foo_owners", "bar_owners")),
    ("has_dependencies_perfile", ("foo_owners", )),
    ("has_dependencies_include", ("foo_owners", )),
)

DEPENDENCY_PATH_TEST_CASES: Iterable[str] = (
    "dependencies_paths_relative",
    "dependencies_paths_absolute",
)

GOOD_TEST_CASES = (("good1", "good1_include", "good1_file"), )

# ===== Unit Tests =====
class TestOwnersChecks(unittest.TestCase):
    """Unittest class for owners_checks.py."""

    maxDiff = 2000

    @staticmethod
    def _create_temp_files(
        temp_dir: str, file_list: Sequence[Tuple[str, str]]
    ) -> Sequence[Tuple[str, pathlib.Path]]:
        real_files = []
        temp_dir_path = pathlib.Path(temp_dir)
        for name, contents in file_list:
            file_path = temp_dir_path / name
            file_path.write_text(contents)
            real_files.append((name, file_path))
        return real_files

    def test_bad_files(self):
        for test_file, expected_exception in BAD_TEST_FILES:
            with self.subTest(i=test_file), tempfile.TemporaryDirectory(
            ) as temp_dir, self.assertRaises(expected_exception):
                file_contents = globals()[test_file]
                real_files = self._create_temp_files(
                    temp_dir=temp_dir,
                    file_list=((test_file, file_contents), ))
                owners_file = owners_checks.OwnersFile(real_files[0][1])
                owners_file.look_for_owners_errors()
                owners_file.check_style()

    def test_good(self):
        for test_files in GOOD_TEST_CASES:
            with self.subTest(i=test_files[0]), tempfile.TemporaryDirectory(
            ) as temp_dir:
                files = [(file_name, globals()[file_name])
                         for file_name in test_files]
                real_files = self._create_temp_files(temp_dir=temp_dir,
                                                     file_list=files)
                self.assertDictEqual({},
                                     owners_checks.run_owners_checks(
                                         real_files[0][1]))

    def test_style_proposals(self):
        for unstyled_file, styled_file in STYLING_CHECKS:
            with self.subTest(i=unstyled_file), tempfile.TemporaryDirectory(
            ) as temp_dir:
                unstyled_contents = globals()[unstyled_file]
                styled_contents = globals()[styled_file]
                real_files = self._create_temp_files(
                    temp_dir=temp_dir,
                    file_list=((unstyled_file, unstyled_contents), ))
                owners_file = owners_checks.OwnersFile(real_files[0][1])
                formatted_content = "\n".join(owners_file.formatted_lines)
                self.assertEqual(styled_contents, formatted_content)

    def test_dependency_discovery(self):

        for file_under_test, expected_deps in DEPENDENCY_TEST_CASES:
            # During test make the test file directory the "git root"
            with tempfile.TemporaryDirectory() as temp_dir:
                with self.subTest(i=file_under_test), mock.patch(
                        "pw_presubmit.owners_checks.git_repo.root",
                        return_value=pathlib.Path(temp_dir)):
                    primary_file = (file_under_test,
                                    globals()[file_under_test])
                    deps_files = tuple(
                        (dep, (globals()[dep])) for dep in expected_deps)

                    primary_files = self._create_temp_files(
                        temp_dir=temp_dir, file_list=(primary_file, ))
                    dep_files = self._create_temp_files(temp_dir=temp_dir,
                                                        file_list=deps_files)

                    owners_file = owners_checks.OwnersFile(primary_files[0][1])
                    found_deps = owners_file.get_dependencies()
                    expected_deps_path = [path for _, path in dep_files]
                    expected_deps_path.sort()
                    found_deps.sort()
                    self.assertListEqual(expected_deps_path, found_deps)

    def test_dependency_path_creation(self):
        """Confirm paths care constructed for absolute and relative paths."""
        for file_under_test in DEPENDENCY_PATH_TEST_CASES:
            # During test make the test file directory the "git root"
            with tempfile.TemporaryDirectory() as temp_dir:
                temp_dir_path = pathlib.Path(temp_dir)
                with self.subTest(i=file_under_test), mock.patch(
                        "pw_presubmit.owners_checks.git_repo.root",
                        return_value=temp_dir_path):
                    owners_file_path = (temp_dir_path / "owners" /
                                        file_under_test)
                    owners_file_path.parent.mkdir(parents=True)
                    owners_file_path.write_text(globals()[file_under_test])
                    owners_file = owners_checks.OwnersFile(owners_file_path)
                    found_deps = owners_file.get_dependencies()

                    if "absolute" in file_under_test:
                        # Absolute paths start with the git/project root
                        expected_prefix = temp_dir_path / "test"
                    else:
                        # Relative paths start with owners file dir
                        expected_prefix = owners_file_path.parent / "foo"

                    expected_deps_path = [
                        (expected_prefix / filename).resolve()
                        for filename in ("include_owners", "file_owners",
                                         "perfile_owners")
                    ]
                    expected_deps_path.sort()
                    found_deps.sort()
                    self.assertListEqual(expected_deps_path, found_deps)

if __name__ == "__main__":
    unittest.main()
<commit_msg>pw_presubmit: Another attempt fixing CI breakage<commit_after># Copyright 2020 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#
"""Unit tests for owners_checks.py."""
import pathlib
import tempfile
from typing import Iterable, Sequence, Tuple
import unittest
from unittest import mock
from pw_presubmit import owners_checks

# ===== Test data =====

bad_duplicate = """\
# Should raise OwnersDuplicateError.
set noparent

file:/foo/OWNERZ
file:../OWNERS
file:../OWNERS
test1@example.com
#Test 2 comment
test2@example.com
"""

bad_duplicate_user = """\
# Should raise OwnersDuplicateError.
set noparent

file:/foo/OWNERZ
file:../OWNERS

*
test1@example.com
#Test 2 comment
test2@example.com
                        test1@example.com
"""

bad_duplicate_wildcard = """\
# Should raise OwnersDuplicateError.
set noparent
*
file:/foo/OWNERZ
file:../OWNERS
test1@example.com
#Test 2 comment
test2@example.com
*
"""

bad_email = """\
# Should raise OwnersInvalidLineError.
set noparent
*
file:/foo/OWNERZ
file:../OWNERS
test1example.com
#Test 2 comment
test2@example.com
*
"""
bad_grant_combo = """\
# Should raise OwnersUserGrantError.

file:/foo/OWNERZ
file:../OWNERS

test1@example.com
#Test noparent comment
set noparent
test2@example.com

*
"""

bad_ordering1 = """\
# Tests formatter reorders groupings of lines into the right order.
file:/foo/OWNERZ
file:bar/OWNERZ

test1@example.com
#Test noparent comment
set noparent
test2@example.com
"""

bad_ordering1_fixed = """\
#Test noparent comment
set noparent

# Tests formatter reorders groupings of lines into the right order.
file:/foo/OWNERZ
file:bar/OWNERZ

test1@example.com
test2@example.com
"""

bad_prohibited1 = """\
# Should raise OwnersProhibitedError.
set noparent

file:/foo/OWNERZ
file:../OWNERS

test1@example.com
#Test 2 comment
test2@example.com

include file1.txt

per-file foo.txt=test3@example.com
"""

bad_moving_comments = """\
# Test comments move with the rule that follows them.
test2@example.com
test1@example.com

# foo comment
file:/foo/OWNERZ
# .. comment
file:../OWNERS

set noparent
"""
bad_moving_comments_fixed = """\
set noparent

# .. comment
file:../OWNERS
# foo comment
file:/foo/OWNERZ

test1@example.com
# Test comments move with the rule that follows them.
test2@example.com
"""

bad_whitespace = """\
 set   noparent

file:/foo/OWNERZ

  file:../OWNERS

test1@example.com
#Test 2    comment
    test2@example.com

"""

bad_whitespace_fixed = """\
set noparent

file:../OWNERS
file:/foo/OWNERZ

test1@example.com
#Test 2    comment
test2@example.com
"""

no_dependencies = """\
# Test no imports are found when there are none.
set noparent

test1@example.com
#Test 2 comment
test2@example.com
"""

has_dependencies_file = """\
# Test if owners checks examine file: imports.
set noparent

file:foo_owners
file:bar_owners

test1@example.com
#Test 2 comment
test2@example.com
"""

has_dependencies_perfile = """\
# Test if owners checks examine per-file imports.
set noparent

test1@example.com
#Test 2 comment
test2@example.com

per-file *.txt=file:foo_owners
per-file *.md=example.google.com
"""

has_dependencies_include = """\
# Test if owners checks examine per-file imports.
set noparent

test1@example.com
#Test 2 comment
test2@example.com

per-file *.txt=file:foo_owners
per-file *.md=example.google.com
"""

dependencies_paths_relative = """\
set noparent

include foo/bar/../include_owners

file:foo/bar/../file_owners

*

per-file *.txt=file:foo/bar/../perfile_owners
"""

dependencies_paths_absolute = """\
set noparent

include /test/include_owners

file:/test/file_owners

*

per-file *.txt=file:/test/perfile_owners
"""

good1 = """\
# Checks should fine this formatted correctly
set noparent

include good1_include

file:good1_file

test1@example.com
#Test 2 comment
test2@example.com #{LAST_RESORT_SUGGESTION}
# LAST LINE
"""

good1_include = """\
test1@example.comom
"""

good1_file = """\
# Checks should fine this formatted correctly.
test1@example.com
"""

foo_owners = """\
test1@example.com
"""

bar_owners = """\
test1@example.com
"""

BAD_TEST_FILES = (
    ("bad_duplicate", owners_checks.OwnersDuplicateError),
    ("bad_duplicate_user", owners_checks.OwnersDuplicateError),
    ("bad_duplicate_wildcard", owners_checks.OwnersDuplicateError),
    ("bad_email", owners_checks.OwnersInvalidLineError),
    ("bad_grant_combo", owners_checks.OwnersUserGrantError),
    ("bad_ordering1", owners_checks.OwnersStyleError),
    ("bad_prohibited1", owners_checks.OwnersProhibitedError),
)

STYLING_CHECKS = (
    ("bad_moving_comments", "bad_moving_comments_fixed"),
    ("bad_ordering1", "bad_ordering1_fixed"),
    ("bad_whitespace", "bad_whitespace_fixed"),
)

DEPENDENCY_TEST_CASES: Iterable[Tuple[str, Iterable[str]]] = (
    ("no_dependencies", tuple()),
    ("has_dependencies_file", ("foo_owners", "bar_owners")),
    ("has_dependencies_perfile", ("foo_owners", )),
    ("has_dependencies_include", ("foo_owners", )),
)

DEPENDENCY_PATH_TEST_CASES: Iterable[str] = (
    "dependencies_paths_relative",
    "dependencies_paths_absolute",
)

GOOD_TEST_CASES = (("good1", "good1_include", "good1_file"), )

# ===== Unit Tests =====
class TestOwnersChecks(unittest.TestCase):
    """Unittest class for owners_checks.py."""

    maxDiff = 2000

    @staticmethod
    def _create_temp_files(
        temp_dir: str, file_list: Sequence[Tuple[str, str]]
    ) -> Sequence[Tuple[str, pathlib.Path]]:
        real_files = []
        temp_dir_path = pathlib.Path(temp_dir)
        for name, contents in file_list:
            file_path = temp_dir_path / name
            file_path.write_text(contents)
            real_files.append((name, file_path))
        return real_files

    def test_bad_files(self):
        for test_file, expected_exception in BAD_TEST_FILES:
            with self.subTest(i=test_file), tempfile.TemporaryDirectory(
            ) as temp_dir, self.assertRaises(expected_exception):
                file_contents = globals()[test_file]
                real_files = self._create_temp_files(
                    temp_dir=temp_dir,
                    file_list=((test_file, file_contents), ))
                owners_file = owners_checks.OwnersFile(real_files[0][1])
                owners_file.look_for_owners_errors()
                owners_file.check_style()

    def test_good(self):
        for test_files in GOOD_TEST_CASES:
            with self.subTest(i=test_files[0]), tempfile.TemporaryDirectory(
            ) as temp_dir:
                files = [(file_name, globals()[file_name])
                         for file_name in test_files]
                real_files = self._create_temp_files(temp_dir=temp_dir,
                                                     file_list=files)
                self.assertDictEqual({},
                                     owners_checks.run_owners_checks(
                                         real_files[0][1]))

    def test_style_proposals(self):
        for unstyled_file, styled_file in STYLING_CHECKS:
            with self.subTest(i=unstyled_file), tempfile.TemporaryDirectory(
            ) as temp_dir:
                unstyled_contents = globals()[unstyled_file]
                styled_contents = globals()[styled_file]
                real_files = self._create_temp_files(
                    temp_dir=temp_dir,
                    file_list=((unstyled_file, unstyled_contents), ))
                owners_file = owners_checks.OwnersFile(real_files[0][1])
                formatted_content = "\n".join(owners_file.formatted_lines)
                self.assertEqual(styled_contents, formatted_content)

    def test_dependency_discovery(self):

        for file_under_test, expected_deps in DEPENDENCY_TEST_CASES:
            # During test make the test file directory the "git root"
            with tempfile.TemporaryDirectory() as temp_dir:
                with self.subTest(i=file_under_test), mock.patch(
                        "pw_presubmit.owners_checks.git_repo.root",
                        return_value=pathlib.Path(temp_dir)):
                    primary_file = (file_under_test,
                                    globals()[file_under_test])
                    deps_files = tuple(
                        (dep, (globals()[dep])) for dep in expected_deps)

                    primary_files = self._create_temp_files(
                        temp_dir=temp_dir, file_list=(primary_file, ))
                    dep_files = self._create_temp_files(temp_dir=temp_dir,
                                                        file_list=deps_files)

                    owners_file = owners_checks.OwnersFile(primary_files[0][1])
                    found_deps = owners_file.get_dependencies()
                    expected_deps_path = [path for _, path in dep_files]
                    expected_deps_path.sort()
                    found_deps.sort()
                    self.assertListEqual(expected_deps_path, found_deps)

    def test_dependency_path_creation(self):
        """Confirm paths care constructed for absolute and relative paths."""
        for file_under_test in DEPENDENCY_PATH_TEST_CASES:
            # During test make the test file directory the "git root"
            with tempfile.TemporaryDirectory() as temp_dir:
                temp_dir_path = pathlib.Path(temp_dir).resolve()
                with self.subTest(i=file_under_test), mock.patch(
                        "pw_presubmit.owners_checks.git_repo.root",
                        return_value=temp_dir_path):
                    owners_file_path = (temp_dir_path / "owners" /
                                        file_under_test)
                    owners_file_path.parent.mkdir(parents=True)
                    owners_file_path.write_text(globals()[file_under_test])
                    owners_file = owners_checks.OwnersFile(owners_file_path)
                    found_deps = owners_file.get_dependencies()

                    if "absolute" in file_under_test:
                        # Absolute paths start with the git/project root
                        expected_prefix = temp_dir_path / "test"
                    else:
                        # Relative paths start with owners file dir
                        expected_prefix = owners_file_path.parent / "foo"

                    expected_deps_path = [
                        (expected_prefix / filename).resolve()
                        for filename in ("include_owners", "file_owners",
                                         "perfile_owners")
                    ]
                    expected_deps_path.sort()
                    found_deps.sort()
                    self.assertListEqual(expected_deps_path, found_deps)

if __name__ == "__main__":
    unittest.main()
<|endoftext|>