Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2e60933
power/quality add basic formatting
akeeste Dec 4, 2023
c5d0949
initial conversion or power/quality submodule
akeeste Dec 4, 2023
97f32cb
add xarray tests for power.quality
akeeste Dec 5, 2023
171bd3e
fix variable assignment in power.quality
akeeste Dec 5, 2023
4d36c8f
power.characteristics add basic formatting for xr conversion
akeeste Dec 5, 2023
ec279ec
update error messages
akeeste Dec 5, 2023
66e0386
finish converting power.quality to xarray
akeeste Dec 18, 2023
ca23206
fix spaces/formating in docstrings
akeeste Dec 18, 2023
cbb882c
clean up conversion of inputs from pandas to xr.dataset
akeeste Dec 19, 2023
976eda1
add tests for xarray
akeeste Dec 19, 2023
ce51185
update handling of timestamps in power.characteristics for xr
akeeste Dec 19, 2023
9c48048
fix length in power.quality.harmonics
akeeste Dec 19, 2023
999ee91
fix length in power.quality.harmonics, again
akeeste Dec 19, 2023
bf7bead
add frequency_dimension and time_dimension arguments
akeeste Dec 19, 2023
f496a84
remove old imports
akeeste Dec 19, 2023
08ba347
remove obsolete argument from THCD tests
akeeste Jan 2, 2024
74acc8c
Merge branch 'develop' of https://github.com/MHKiT-Software/MHKiT-Pyt…
akeeste Jan 25, 2024
480005a
type check on to_pandas
akeeste Jan 25, 2024
62575c2
add type and value checks for time_dimension and frequency_dimension
akeeste Jan 25, 2024
d0586cb
make grid_freq checks f strings that return the incorrect value
akeeste Jan 25, 2024
6e067cd
add frequency_dimension valueError
akeeste Jan 26, 2024
1aeb816
add formal docstring to _convert_to_dataset
akeeste Jan 26, 2024
4c7e312
add line_to_line type check
akeeste Jan 26, 2024
d8d9a46
restore old naming convention to ac_power_three_phase
akeeste Jan 26, 2024
15e162d
update example call to THCD
akeeste Jan 26, 2024
10e07f8
return hard coded test answers to being recalculated
akeeste Jan 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 65 additions & 84 deletions examples/power_example.ipynb

Large diffs are not rendered by default.

262 changes: 195 additions & 67 deletions mhkit/power/characteristics.py
Original file line number Diff line number Diff line change
@@ -1,121 +1,249 @@
import pandas as pd
import xarray as xr
import numpy as np
from scipy.signal import hilbert
import datetime

def instantaneous_frequency(um):
def instantaneous_frequency(um, time_dimension="", to_pandas=True):

"""
Calculates instantaneous frequency of measured voltage



Parameters
-----------
um: pandas Series or DataFrame
Measured voltage (V) indexed by time
um: pandas Series, pandas DataFrame, xarray DataArray, or xarray Dataset
Measured voltage (V) indexed by time

time_dimension: string (optional)
Name of the xarray dimension corresponding to time. If not supplied,
defaults to the first dimension. Does not affect pandas input.

to_pandas: bool (Optional)
Flag to save output to pandas instead of xarray. Default = True.


Returns
---------
frequency: pandas DataFrame
Frequency of the measured voltage (Hz) indexed by time
frequency: pandas DataFrame or xarray Dataset
Frequency of the measured voltage (Hz) indexed by time
with signal name columns
"""
if not isinstance(um, (pd.Series, pd.DataFrame)):
raise TypeError(f'um must be of type pd.Series or pd.DataFrame. Got: {type(um)}')

if isinstance(um.index[0], datetime.datetime):
t = (um.index - datetime.datetime(1970,1,1)).total_seconds()
else:
t = um.index
if not isinstance(um, (pd.Series, pd.DataFrame, xr.DataArray, xr.Dataset)):
raise TypeError('um must be of type pd.Series, pd.DataFrame, ' +
f'xr.DataArray, or xr.Dataset. Got {type(um)}')
if not isinstance(to_pandas, bool):
raise TypeError(
f'to_pandas must be of type bool. Got: {type(to_pandas)}')
if not isinstance(time_dimension, str):
raise TypeError(
f'time_dimension must be of type bool. Got: {type(time_dimension)}')

# Convert input to xr.Dataset
um = _convert_to_dataset(um, 'data')

if time_dimension != '' and time_dimension not in um.coords:
raise ValueError('time_dimension was supplied but is not a dimension '
+ f'of um. Got {time_dimension}')

dt = pd.Series(t).diff()[1:]
# Get the dimension of interest
if time_dimension == "":
time_dimension = list(um.coords)[0]

if isinstance(um,pd.Series):
um = um.to_frame()
# Calculate time step
if isinstance(um.coords[time_dimension].values[0], np.datetime64):
t = (um[time_dimension] - np.datetime64('1970-01-01 00:00:00'))/np.timedelta64(1, 's')
else:
t = um[time_dimension]
dt = np.diff(t)

columns = um.columns
frequency=pd.DataFrame(columns=columns)
for column in um.columns:
f = hilbert(um[column])
# Calculate frequency
frequency = xr.Dataset()
for var in um.data_vars:
f = hilbert(um[var])
instantaneous_phase = np.unwrap(np.angle(f))
instantaneous_frequency = np.diff(instantaneous_phase) /(2.0*np.pi) * (1/dt)
frequency[column] = instantaneous_frequency

instantaneous_frequency = np.diff(instantaneous_phase)/(2.0*np.pi) * (1/dt)

frequency = frequency.assign({var: (time_dimension, instantaneous_frequency)})
frequency = frequency.assign_coords({time_dimension: um.coords[time_dimension].values[0:-1]})

if to_pandas:
frequency = frequency.to_pandas()

return frequency

def dc_power(voltage, current):
def dc_power(voltage, current, to_pandas=True):
"""
Calculates DC power from voltage and current

Parameters
-----------
voltage: pandas Series or DataFrame
voltage: pandas Series, pandas DataFrame, xarray DataArray, or xarray Dataset
Measured DC voltage [V] indexed by time
current: pandas Series or DataFrame

current: pandas Series, pandas DataFrame, xarray DataArray, or xarray Dataset
Measured three phase current [A] indexed by time


to_pandas: bool (Optional)
Flag to save output to pandas instead of xarray. Default = True.

Returns
--------
P: pandas DataFrame
P: pandas DataFrame or xarray Dataset
DC power [W] from each channel and gross power indexed by time
"""
if not isinstance(voltage, (pd.Series, pd.DataFrame)):
raise TypeError(f'voltage must be of type pd.Series or pd.DataFrame. Got: {type(voltage)}')
if not isinstance(current, (pd.Series, pd.DataFrame)):
raise TypeError(f'current must be of type pd.Series or pd.DataFrame. Got: {type(current)}')
if not voltage.shape == current.shape:
raise ValueError('current and volatge must have the same shape')

if not isinstance(voltage, (pd.Series, pd.DataFrame, xr.DataArray, xr.Dataset)):
raise TypeError('voltage must be of type pd.Series, pd.DataFrame, ' +
f'xr.DataArray, or xr.Dataset. Got {type(voltage)}')
if not isinstance(current, (pd.Series, pd.DataFrame, xr.DataArray, xr.Dataset)):
raise TypeError('current must be of type pd.Series, pd.DataFrame, ' +
f'xr.DataArray, or xr.Dataset. Got {type(current)}')
if not isinstance(to_pandas, bool):
raise TypeError(
f'to_pandas must be of type bool. Got: {type(to_pandas)}')

# Convert inputs to xr.Dataset
voltage = _convert_to_dataset(voltage, 'voltage')
current = _convert_to_dataset(current, 'current')

# Check that sizes are the same
if not (voltage.sizes == current.sizes and len(voltage.data_vars) == len(current.data_vars)):
raise ValueError('current and voltage must have the same shape')

P = xr.Dataset()
gross = None

P = current.values * voltage.values
P = pd.DataFrame(P)
P['Gross'] = P.sum(axis=1, skipna=True)
# Multiply current and voltage variables together, in order they're assigned
for i, (current_var, voltage_var) in enumerate(zip(current.data_vars,voltage.data_vars)):
temp = current[current_var]*voltage[voltage_var]
P = P.assign({f'{i}': temp})
if gross is None:
gross = temp
else:
gross = gross + temp

P = P.assign({'Gross': gross})

if to_pandas:
P = P.to_dataframe()

return P

def ac_power_three_phase(voltage, current, power_factor, line_to_line=False):
def ac_power_three_phase(voltage, current, power_factor, line_to_line=False, to_pandas=True):
"""
Calculates magnitude of active AC power from line to neutral voltage and current

Parameters
-----------
voltage: pandas DataFrame
Time-series of three phase measured voltage [V] indexed by time
current: pandas DataFrame
Time-series of three phase measured current [A] indexed by time
voltage: pandas Series, pandas DataFrame, xarray DataArray, or xarray Dataset
Measured DC voltage [V] indexed by time

current: pandas Series, pandas DataFrame, xarray DataArray, or xarray Dataset
Measured three phase current [A] indexed by time

power_factor: float
Power factor for the efficiency of the system
line_to_line: bool

line_to_line: bool (Optional)
Set to true if the given voltage measurements are line_to_line


to_pandas: bool (Optional)
Flag to save output to pandas instead of xarray. Default = True.

Returns
--------
P: pandas DataFrame
P: pandas DataFrame or xarray Dataset
Magnitude of active AC power [W] indexed by time with Power column
"""
if not isinstance(voltage, pd.DataFrame):
raise TypeError(f'voltage must be of type pd.DataFrame. Got: {type(voltage)}')
if not isinstance(current, pd.DataFrame):
raise TypeError(f'current must be of type pd.DataFrame. Got: {type(current)}')
if not len(voltage.columns) == 3:
if not isinstance(voltage, (pd.Series, pd.DataFrame, xr.DataArray, xr.Dataset)):
raise TypeError('voltage must be of type pd.Series, pd.DataFrame, ' +
f'xr.DataArray, or xr.Dataset. Got {type(voltage)}')
if not isinstance(current, (pd.Series, pd.DataFrame, xr.DataArray, xr.Dataset)):
raise TypeError('current must be of type pd.Series, pd.DataFrame, ' +
f'xr.DataArray, or xr.Dataset. Got {type(current)}')
if not isinstance(line_to_line, bool):
raise TypeError(
f'line_to_line must be of type bool. Got: {type(line_to_line)}')
if not isinstance(to_pandas, bool):
raise TypeError(
f'to_pandas must be of type bool. Got: {type(to_pandas)}')

# Convert inputs to xr.Dataset
voltage = _convert_to_dataset(voltage, 'voltage')
current = _convert_to_dataset(current, 'current')

# Check that sizes are the same
if not len(voltage.data_vars) == 3:
raise ValueError('voltage must have three columns')
if not len(current.columns) == 3:
if not len(current.data_vars) == 3:
raise ValueError('current must have three columns')
if not current.shape == voltage.shape:
if not current.sizes == voltage.sizes:
raise ValueError('current and voltage must be of the same size')


abs_current = np.abs(current.values)
abs_voltage = np.abs(voltage.values)
power = dc_power(voltage, current, to_pandas=False)['Gross']
power.name = 'Power'
power = power.to_dataset() # force xr.DataArray to be consistently in xr.Dataset format
Comment on lines +180 to +182
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This forcing seems like a intermediate step needed prior to getting dc_power to return an xarray.

Is this still needed when we have to_pandas=False?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the Gross power variable is extracted from the results of dc_power, the xr.Dataset automatically becomes an xr.DataArray since there's just one variable. I originally added this conversion to a xr.Dataset to prevent that type change.

There shouldn't be any issues with returning a single variable array like pd.Series or xr.DataArray, but the previous function and most of the current ones I'm working with return pd.DataFrame and xr.Dataset

P = np.abs(power) * power_factor

if line_to_line:
power = abs_current * (abs_voltage * np.sqrt(3))
else:
power = abs_current * abs_voltage

power = pd.DataFrame(power)
P = power.sum(axis=1) * power_factor
P = P.to_frame('Power')

P = P * np.sqrt(3)

if to_pandas:
P = P.to_pandas()

return P

def _convert_to_dataset(data, name='data'):
"""
Converts the given data to an xarray.Dataset.

This function is designed to handle inputs that can be either a pandas DataFrame, a pandas Series,
an xarray DataArray, or an xarray Dataset. It ensures that the output is consistently an xarray.Dataset.

Parameters
----------
data: pandas DataFrame, pandas Series, xarray DataArray, or xarray Dataset
The data to be converted.

name: str (Optional)
The name to assign to the data variable in case the input is an xarray DataArray without a name.
Default value is 'data'.

Returns
-------
xarray.Dataset
The input data converted to an xarray.Dataset. If the input is already an xarray.Dataset,
it is returned as is.

Examples
--------
>>> df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
>>> ds = _convert_to_dataset(df)
>>> type(ds)
<class 'xarray.core.dataset.Dataset'>

>>> series = pd.Series([1, 2, 3], name='C')
>>> ds = _convert_to_dataset(series)
>>> type(ds)
<class 'xarray.core.dataset.Dataset'>

>>> data_array = xr.DataArray([1, 2, 3])
>>> ds = _convert_to_dataset(data_array, name='D')
>>> type(ds)
<class 'xarray.core.dataset.Dataset'>
"""
if not isinstance(data, (pd.DataFrame, pd.Series, xr.DataArray, xr.Dataset)):
raise TypeError("Input data must be of type pandas.DataFrame, pandas.Series, "
"xarray.DataArray, or xarray.Dataset")

if not isinstance(name, str):
raise TypeError("The 'name' parameter must be a string")

# Takes data that could be pd.DataFrame, pd.Series, xr.DataArray, or
# xr.Dataset and converts it to xr.Dataset
if isinstance(data, (pd.DataFrame, pd.Series)):
data = data.to_xarray()

if isinstance(data, xr.DataArray):
if data.name is None:
data.name = name # xr.DataArray.to_dataset() breaks if the data variable is unnamed
data = data.to_dataset()

return data
Loading