Source code for array_collections._property_array

# -*- coding: utf-8 -*-
"""
Created on Thu Jan 10 03:56:02 2019

@author: yoelr
"""
import numpy as np
from pandas import DataFrame
from ._tuple_array import tuple_array
from free_properties._free_property import inplace_magic_names
import re

__all__ = ('property_array',)

ndarray = np.ndarray
asarray = np.asarray
getitem = ndarray.__getitem__
broadcast = np.broadcast_to

# %% Functions

# def has_property(array):
#     return (0 in array.shape or 
#             hasattr(ndarray.__getitem__(array, (0,)*array.ndim), 'value'))

def make_1d_table(data, with_units, ndim):
    """Create a 1d DataFrame object if all entries have the same type."""
    index = []; cols = []
    # Add index
    for i in data:
        index.append(str(i.name))
    
    # Make sure all items have same type
    i0 = data.item(0)
    if not (np.array([type(i) for i in data]) == type(i0)).all():
        return None
    
    # Add column
    colname = type(i0).__name__
    colname = re.sub(r"\B([A-Z])", r" \1", colname).capitalize()
    units = f' ({i0._units})' if with_units else ''
    cols.append(colname + units)
    
    return DataFrame(data, index=index, columns=cols)
    
def make_2d_table(data, with_units, ndim):
    """Create a 2d DataFrame object if all columns and rows have the same type and name attribute respectively."""
    index = []; cols = []
    for r in data:
        # Add index
        i0 = r.item(0)
        name0 = str(i0.name)
        index.append(name0)
        
        # Make sure the whole row is of same type
        names = np.array([str(i.name) for i in r])
        if not (names == name0).all(): return None
    
    for r in data.transpose():
        # Add column
        Type0 = type(r.item(0))
        colname = Type0.__name__
        colname = re.sub(r"\B([A-Z])", r" \1", colname).capitalize()
        units = f' ({i0._units})' if with_units else ''
        cols.append(colname + units)
        
        # Make sure the whole column is of same type
        Types = np.array([type(i) for i in r])
        if not (Types == Type0).all(): return None    

    return DataFrame(data, index=index, columns=cols)


# %% Property array

[docs]class property_array(ndarray): """Create an array that allows for array-like manipulation of FreeProperty objects. All entries in a property_array must be instances of FreeProperty. Setting items of a property_array sets values of objects instead. **Parameters** **array:** array_like[FreeProperty] Input data, in any form that can be converted to an array. This includes lists, lists of tuples, tuples, tuples of tuples, tuples of lists and ndarrays. **order:** {'C', 'F'} Whether to use row-major (C-style) or column-major (Fortran-style) memory representation. Defaults to ā€˜Cā€™. **Examples** Use the PropertyFactory to create a Weight property class which calculates weight based on density and volume: .. code-block:: python from array_collections import PropertyFactory >>> @PropertyFactory >>> def Weight(self): ... '''Weight (kg) based on volume (m^3).''' ... data = self.data ... rho = data['rho'] # Density (kg/m^3) ... vol = data['vol'] # Volume (m^3) ... return rho * vol >>> >>> @Weight.setter >>> def Weight(self, weight): ... data = self.data ... rho = data['rho'] # Density (kg/m^3) ... data['vol'] = weight / rho Create dictionaries of data and initialize new properties: .. code-block:: python >>> water_data = {'rho': 1000, 'vol': 3} >>> ethanol_data = {'rho': 789, 'vol': 3} >>> weight_water = Weight('Water', water_data) >>> weight_ethanol = Weight('Ethanol', ethanol_data) >>> weight_water Weight(Water) -> 3000 (kg) >>>weight_ethanol Weight(Ethanol) -> 2367 (kg) Create a property_array from data: .. code-block:: python >>> prop_arr = property_array([weight_water, weight_water]) property_array([3000, 2367]) Changing the values of a property_array changes the value of its properties: .. code-block:: python >>> # Addition in place >>> prop_arr += 3000 >>> prop_arr property_array([6000, 5367]) >>> # Note how the data also changes >>> water_data {'rho': 1000, 'vol': 6.0} >>> ethanol_data {'rho': 789, 'vol': 6.802281368821292} >>> # Setting an item changes the property value >>> prop_arr[1] = 2367 >>> ethanol_data {'rho': 789, 'vol': 3} New arrays have no connection to the property_array: .. code-block:: python >>> prop_arr - 1000 # Returns a new array array([5000.0, 1367.0], dtype=object) >>> water_data # Data remains unchanged {'rho': 1000, 'vol': 6.0} A representative DataFrame can also be made from the property_array: .. code-block:: python >>> prop_arr.table() Weight (kg) Water 6000.0 Ethanol 2367.0 .. Note:: The DataFrame object contains the values of the properties, not the FreeProperty objects as a property_array would. """ __slots__ = () def __new__(cls, properties, order='C'): return asarray(properties, object, order).view(cls) def __getitem__(self, key): value = getitem(self, key) if isinstance(value, property_array): return value else: return value.value def __setitem__(self, key, value): items = self.view(ndarray)[key] if isinstance(items, ndarray): for i, v in zip(items.flatten(), broadcast(value, items.shape).flatten()): i.value = v else: items.value = value def __array_wrap__(self, result): if self is result: return self else: return result.view(ndarray)
[docs] def table(self, title='', with_units=True): """Create a representative DataFrame object.""" ndim = self.ndim if ndim == 1: make_table = make_1d_table elif ndim == 2: make_table = make_2d_table else: raise ValueError(f'Dimension of table must be either 1 or 2, not {self.ndim}') data = self.view(ndarray) # First assume all columns and rows have the same type and name respectively table = make_table(data, with_units, ndim) if table is None: # When this fails, assume all rows have same type and columns have same name data = data.transpose() table = make_table(data, with_units, ndim) if table is None: raise TypeError('To create a table, at least one dimension must have the same property type and the other dimension the same property name.') else: table = table.transpose() table.title = title return table
def __repr__(self): if len(self) == 0: return tuple_array.__repr__(self) else: return tuple_array.__repr__(self.astype(type(self.item(0).value)))
def wrap_ifunc(iname): name = iname[:2] + iname[3:] # Remove 'i' if hasattr(property_array, name): func = getattr(property_array, name) def wrapper(self, *args, **kwargs): self[:] = func(self, *args, **kwargs) return self wrapper.__name__ = iname return wrapper for iname in inplace_magic_names: setattr(property_array, iname, wrap_ifunc(iname))