Commit d4155c48 authored by hazrmard's avatar hazrmard
Browse files

added unit conversion based on field name guessing, version bump to 0.2.0

parent 4d4e3d93
Loading
Loading
Loading
Loading
+29 −3
Original line number Diff line number Diff line
@@ -5,16 +5,35 @@ Python package/script for downloading trends from Building Logix Data Exchange (
## Installation

```
pip install git+https://git.isis.vanderbilt.edu/SmartBuildings/bdx@v0.1.0
pip install git+https://git.isis.vanderbilt.edu/SmartBuildings/bdx@v0.2.0
```

## Usage

### Command line

```
python -m bdx user password trend start end file
```bash
usage: bdx [-h] [--file FILE] [-p] user password trend start end

Download data from Building Logix Data Exchange (BDX) and save it to csv.

positional arguments:
  user              Username for BDX
  password          Password for BDX
  trend             ID of trend to download
  start             Start time in ISO 8601 format.
  end               Start time in ISO 8601 format.

optional arguments:
  -h, --help        show this help message and exit
  --file FILE       Filenname to save CSV. If not provided, printed to console
  -p, --preprocess  Convert to SI units by guessing field names.

Date/time formats can be
 - 2020-02-03T15:30:00Z-6 (i.e. Feb 3 2020 3:30 PM UTC-6 timezone)
 - 20200203T153000Z-6

# for example:
python -m bdx harrypotter wing@rd1umLeviosa 2422 2020-02-03T15:00Z-6 2020-02-04T16:00Z-6 ~/data.csv
```

@@ -39,6 +58,13 @@ dataframe = get_trend(trend_id='2422',
                      password='wing@rd1umLeviosa',
                      start=start,
                      end=end)

# Optionally, to convert fields to SI units
from bdx import apply_heuristics

dataframe, left = apply_heuristiccs(dataframe)
print('The following fields did not match and were note converted:', left)

```

## Requirements
+1 −0
Original line number Diff line number Diff line
from .data import get_trend
from .preprocess import apply_heuristics
+19 −7
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ from argparse import ArgumentParser, RawDescriptionHelpFormatter
from dateutil.parser import parse

from .data import get_trend
from .preprocess import apply_heuristics

parser = ArgumentParser(
    prog='bdx',
@@ -13,13 +14,16 @@ parser = ArgumentParser(
            ' - 2020-02-03T15:30:00Z-6 (i.e. Feb 3 2020 3:30 PM UTC-6 timezone)\n'
            ' - 20200203T153000Z-6\n'),
    formatter_class=RawDescriptionHelpFormatter)
parser.add_argument('user', help='Username for BDX', type=str)
parser.add_argument('password', help='Password for BDX', type=str)
parser.add_argument('trend', help='ID of trend to download', type=str)
parser.add_argument('start', help='Start time in ISO 8601 format.', type=parse)
parser.add_argument('end', help='Start time in ISO 8601 format.', type=parse)
parser.add_argument('--file', help=('Filenname to save CSV. If not provided, '
    'printed to console'), type=str, default=sys.stdout)
parser.add_argument('user', type=str, help='Username for BDX')
parser.add_argument('password', type=str, help='Password for BDX')
parser.add_argument('trend', type=str, help='ID of trend to download')
parser.add_argument('start', type=parse, help='Start time in ISO 8601 format.')
parser.add_argument('end', type=parse, help='Start time in ISO 8601 format.')
parser.add_argument('--file',type=str, default=sys.stdout,
                    help=('Filenname to save CSV. If not provided, '
                          'printed to console'))
parser.add_argument('-p', '--preprocess', action='store_true', default=False,
                    help='Convert to SI units by guessing field names.')

args = parser.parse_args()

@@ -29,4 +33,12 @@ trend = get_trend(trend_id=args.trend,
                  start=args.start,
                  end=args.end)

if args.preprocess:
    trend, left = apply_heuristics(trend)
    if len(left) > 0:
        print('The following fields did not match and were not converted:',
              file=sys.stderr)
        for field in left:
            print(field, file=sys.stderr)

trend.to_csv(args.file)

bdx/preprocess.py

0 → 100644
+61 −0
Original line number Diff line number Diff line
"""
Rudimentary preprocessing for data by guessing column names:

- Temperatures (F to K)
- Power (kW, BTU to W)
- Flow (Gallons/min to m^3/s)
- Pressure (PSI to Pascals)
"""



import re
from typing import Tuple, List, Callable

import pandas as pd

from .thermo import f2k, gpm2m3s, psi2pa


HEURISTICS = (
    ('_Temp|Temp$|_Humidity|Wet_Bulb',      f2k),
    ('Kw$',                                 lambda kw: 1000 * kw),
    ('Power$',                              lambda p: p),
    ('Percent$',                            lambda pct: 0.01 * pct),
    ('Flow$',                               gpm2m3s),
    ('Press$',                              psi2pa),
)



def apply_heuristics(df: pd.DataFrame,
                     heuristics: Tuple[Tuple[str, Callable]] = HEURISTICS) \
                    -> Tuple[pd.DataFrame, List[str]]:
    """
    Apply functions to columns based on matching field names.

    Parameters
    ----------
    df : pd.DataFrame
        The dataframe which is processed.
    heuristics : Tuple[Tuple[str, Callable]], optional
        A tuple of tuples, where each tuple containes a regular expression to match
        field names followed by a function that takes a column and returns a converted
        column, by default bdx.preprocess.HEURISTICS
    
    Returns
    -------
    Tuple[pd.DataFrame, List[str]]
        The converted dataframe, and a list of column names which were not
        matched.
    """
    left = []
    for column in df:
        matched = False
        for pattern, func in heuristics:
            if re.search(pattern, column) is not None:
                matched = True
                df[column] = func(df[column])
        if not matched:
            left.append(column)
    return df, left

bdx/thermo.py

0 → 100644
+201 −0
Original line number Diff line number Diff line
"""
Contains formulae for thermodynamics operations.

Adapted from https://git.isis.vanderbilt.edu/SmartBuildings/EngineeringScienceBuilding/blob/0ffc3c8234ecca50606cfd9ed1e1d464f998b832/src/thermo.py
"""

from collections import namedtuple
from numbers import Number

import numpy as np
from scipy.optimize import newton

# physics constants
_Constants = namedtuple('Constants', field_names = (
    'cv',   # specific volumetric heat capacity of water (J / m^3 . K)
    'cm',   # specific mass heat capacity of water (J / kg .K)
))
CONSTANTS = _Constants(cv=4.1796e6, cm=4.1813e3)


# Constants used by vaporpressure() and dewpoint()
_ABConstants = namedtuple('ArdenBuckConstants', 'a b c d')
ABC = _ABConstants(a=0.61121, b=18.678, c=257.14, d=234.5)

# Temperature conversion functions
# f: Farenheit
# c: Celsius
# k: Kelvin

def f2c(f: float): return (f - 32) / 1.8

def f2k(f: float): return c2k(f2c(f))

def c2f(c: float): return (c * 1.8) + 32

def c2k(c: float): return c + 273.15

def k2c(k: float): return k - 273.15

def k2f(k: float): return c2f(k2c(k))

# Energy conversion functions
# btu: British Thermal Unit
# j: Joule

def btu2j(btu: float): return btu * 1055.056

def j2btu(j: float): return j / 1055.056

# Power conversion functions
# btuhr: BTU / hour
# w: Watt
# ton: Ton

def btuhr2w(btu: float): return btu2j(btu) / 3600

def btuhr2ton(btuhr: float): return btuhr / 12000

def ton2btuhr(ton: float): return ton * 12000

def ton2w(ton: float): return btuhr2w(ton2btuhr(ton))

def w2btuhr(w: float): return j2btu(w * 3600)

def w2ton(w: float): return btuhr2ton(w2btuhr(w))

# Flow rate / volume conversion functions
# gph: Gallons Per Hour
# gpm: Gallons Per Minute
# m3s: Cubic Metres per second

def gph2m3s(gph: float): return gph * 3.7854e-3 / 3600

def gpm2m3s(gpm: float): return gpm * 3.7854e-3 / 60



# Pressure conversion

def psi2pa(psi: float): return psi * 6894.75728



def vaporpressure(t: float) -> float:
    """
    Uses Arden Buck equation to find saturation vapor pressure from ambient
    temperature in Kelvin.

    Parameters
    ----------
    t: float
        Temperature in Kelvin.

    Returns
    -------
    float
        The vapor pressure in Pascals.
    """
    t = k2c(t)
    arg = (ABC.b - t / ABC.d) * (t / (ABC.c + t))
    return ABC.a * np.exp(arg) * 1000



def dewpoint(t: float, rh: float) -> float:
    """
    Uses Magnus formula enhanced w/ Arden Buck constants to calculate dew point
    temperature.

    Parameters
    ----------
    t: float
        Temperature in Kelvin.
    rh: float
        Relative humidity [0-1].

    Returns
    -------
    float
        The dew point temperature in Kelvin.
    """
    t = k2c(t)
    arg = (ABC.b - t / ABC.d) * (t / (ABC.c + t))
    gamma = np.log(rh * 100 * np.exp(arg) / 100)
    return c2k((ABC.c * gamma) / (ABC.b - gamma))



def wetbulb(t: float, rh: float) -> float:
    """
    Uses Roland Stull's formula to calculate wet bulb temperature from ambient
    temperature and relative humidity.

    Parameters
    ----------
    t: float
        Temperature in Kelvin.
    rh: float
        Relative humidity [0-1].
    
    Returns
    -------
    float
        The wet bulb temperature in Kelvin.
    """
    t = k2c(t)
    rh *= 100   # convert from fraction to percentage
    return c2k(
            t * np.arctan(0.151977 * np.sqrt(rh + 8.313659))
            + np.arctan(t + rh) - np.arctan(rh - 1.676331)
            + 0.00391838 * np.power(rh, 1.5) * np.arctan(0.023101 * rh)
            - 4.686035)



def _stull_eq(t, rh, tw):
    """
    Sets up the stull equation in the form:

    `f(t, rh) = tw => f(t, rh) - tw = 0`

    The new form can be represented as:

    `g(t) = f(t, rh) - tw`

    The root of `g(t)` is the ambient temperature. Used by `tambient()`. All
    temperatues in the equation are Celsius.
    """
    rh *= 100   # convert from fraction to percentage
    return - tw + (t * np.arctan(0.151977 * np.sqrt(rh + 8.313659))
              + np.arctan(t + rh) - np.arctan(rh - 1.676331)
              + 0.00391838 * np.power(rh, 1.5) * np.arctan(0.023101 * rh)
              - 4.686035)



def ambient(tw: float, rh: float) -> float:
    """
    Uses Roland Stull's formula and Newton's method to calculate ambient
    temperature from wet bulb temperature and relative humidity.

    Parameters
    ----------
    tw: float
        Temperature in Kelvin.
    rh: float
        Relative humidity [0-1].

    Returns
    -------
    float
        The ambient temperature in Kelvin.
    """
    tw = k2c(tw)
    if isinstance(tw, Number) and isinstance(rh, Number):
        return c2k(newton(_stull_eq, x0=tw, args=(rh, tw)))
    else:
        t = np.zeros(len(tw))
        for i, (tw_, rh_) in enumerate(zip(tw, rh)):
            t[i] = newton(_stull_eq, x0=tw_, args=(rh_, tw_))
        return c2k(t)
Loading