Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import example chamber data #404

Merged
merged 9 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
3 changes: 3 additions & 0 deletions docs/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ parts:
- file: examples/loading_data_part4
- file: examples/stream_stats_part1
- file: examples/stream_stats_size_distribution_part2
- file: examples/wall_loss_section
sections:
- file: examples/chamber_smps_data
- file: examples/activity_part1
- file: examples/equilibria_part1
- caption: Documentation
Expand Down
464 changes: 464 additions & 0 deletions docs/examples/chamber_smps_data.ipynb
Gorkowski marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/examples/wall_loss_section.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Chamber Wall Loss Analysis

In this example we'll go through the steps of combining the data analysis,
and modeling capabilities of particula to analyze the wall loss in a
chamber.
26 changes: 17 additions & 9 deletions particula/data/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import List, Union
from dataclasses import dataclass, field
import numpy as np
from particula.util import convert
from particula.util import time_manage


@dataclass
Expand Down Expand Up @@ -68,15 +68,18 @@ def __getitem__(self, index: Union[int, str]):
return self.data[index, :]

def __setitem__(self, index: Union[int, str], value):
"""Allows for setting of a row of data in the stream.
"""Allows for setting or adding of a row of data in the stream.
Args:
----------
index : int or str
The index of the data stream to set.
value : np.ndarray
The data to set at the specified index."""
index : The index of the data stream to set.
value : The data to set at the specified index.

future work maybe add a list option and iterate through the list"""
if isinstance(index, str):
if index not in self.header:
self.header.append(index) # add new header element
self.data = np.vstack((self.data, value))
index = self.header.index(index)
# if index is an int, set the data at that index
self.data[index, :] = value

def __len__(self):
Expand All @@ -89,14 +92,19 @@ def datetime64(self) -> np.ndarray:
Returns an array of datetime64 objects representing the time stream.
Useful for plotting, with matplotlib.dates.
"""
return convert.datetime64_from_epoch_array(self.time)
return time_manage.datetime64_from_epoch_array(self.time)

@property
def return_header_dict(self) -> dict:
def header_dict(self) -> dict:
"""Returns the header as a dictionary with index (0, 1) as the keys
and the names as values."""
return dict(enumerate(self.header))

@property
def header_float(self) -> np.ndarray:
"""Returns the header as a numpy array of floats."""
return np.array(self.header, dtype=float)


@dataclass
class StreamAveraged(Stream):
Expand Down
35 changes: 35 additions & 0 deletions particula/data/stream_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,38 @@ def filtering(
# No need to modify 'stream.time' as it remains consistent with
# 'stream.data'
return stream


def remove_time_window(
stream: Stream,
epoch_start: Union[float, int],
epoch_end: Optional[Union[float, int]] = None,
) -> Stream:
"""
Remove a time window from a stream object.

Args:
- stream: The input stream object containing 'data' and 'time'
attributes.
- epoch_start: The start time of the time window to be
removed.
- epoch_end: The end time of the time window to be
removed. If not provided, the time window is the closest time point to
'epoch_start'.

Returns:
- Stream: The 'stream' object with the specified time window removed.
"""
# get index of start time
index_start = np.argmin(np.abs(stream.time - epoch_start))
if epoch_end is None:
# if no end time provided, remove the closest time point
stream.time = np.delete(stream.time, index_start)
stream.data = np.delete(stream.data, index_start, axis=1)
return stream
# get index of end time
index_end = np.argmin(np.abs(stream.time - epoch_end)) + 1
# remove time and data between start and end times
stream.time = np.delete(stream.time, slice(index_start, index_end))
stream.data = np.delete(stream.data, slice(index_start, index_end), axis=1)
return stream

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"relative_data_folder": "chamber_data",
"filename_regex": "*.csv",
"MIN_SIZE_BYTES": 10,
"data_loading_function": "general_1d_load",
"header_row": 24,
"data_checks": {
"characters": [
250
],
"skip_rows": 25,
"skip_end": 0,
"char_counts": {
"/": 2,
":": 2
}
},
"data_column": [
"Lower Size (nm)",
"Upper Size (nm)",
"Sample Temp (C)",
"Sample Pressure (kPa)",
"Relative Humidity (%)",
"Median (nm)",
"Mean (nm)",
"Geo. Mean (nm)",
"Mode (nm)",
"Geo. Std. Dev.",
"Total Conc. (#/cm\u00b3)"
],
"data_header": [
"Lower_Size_(nm)",
"Upper_Size_(nm)",
"Sample_Temp_(C)",
"Sample_Pressure_(kPa)",
"Relative_Humidity_(%)",
"Median_(nm)",
"Mean_(nm)",
"Geo_Mean_(nm)",
"Mode_(nm)",
"Geo_Std_Dev.",
"Total_Conc_(#/cc)"
],
"time_column": [
1,
2
],
"time_format": "%m/%d/%Y %H:%M:%S",
"delimiter": ",",
"time_shift_seconds": 0,
"timezone_identifier": "UTC"
}
Gorkowski marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"relative_data_folder": "chamber_data",
"filename_regex": "*.csv",
"MIN_SIZE_BYTES": 10,
"data_loading_function": "general_2d_load",
"header_row": 24,
"data_checks": {
"characters": [
250
],
"skip_rows": 25,
"skip_end": 0,
"char_counts": {
"/": 2,
":": 2
}
},
"data_sizer_reader": {
"Dp_start_keyword": "15.82",
"Dp_end_keyword": "756.67",
"convert_scale_from": "dw/dlogdp"
},
"time_column": [
1,
2
],
"time_format": "%m/%d/%Y %H:%M:%S",
"delimiter": ",",
"time_shift_seconds": 0,
"timezone_identifier": "UTC"
}
33 changes: 4 additions & 29 deletions particula/util/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,6 @@ def convert_sizer_dn(
"""
assert len(diameter) == len(dn_dlogdp) > 0, \
"Inputs must be non-empty arrays of the same length."

# Compute the bin widths
delta = np.zeros_like(diameter)
delta[:-1] = np.diff(diameter)
Expand All @@ -419,41 +418,17 @@ def convert_sizer_dn(
lower = diameter - delta / 2
upper = diameter + delta / 2

if dn_dlogdp.ndim == 2:
# expand diameter by one dimension so it can be broadcast
lower = np.expand_dims(lower, axis=1)
upper = np.expand_dims(upper, axis=1)
Gorkowski marked this conversation as resolved.
Show resolved Hide resolved
if inverse:
# Convert from dn to dn/dlogdp
return dn_dlogdp / np.log10(upper / lower)

return dn_dlogdp * np.log10(upper / lower)


def datetime64_from_epoch_array(
epoch_array: np.ndarray,
delta: int = 0) -> np.ndarray:
"""
Converts an array of epoch times to a numpy array of datetime64 objects.

Args:
-----------
epoch_array (np.ndarray): Array of epoch times (in seconds since
the Unix epoch).
delta (int): An optional offset (in seconds) to add to the epoch times
before converting to datetime64 objects.

Returns:
--------
np.ndarray: Array of datetime64 objects corresponding to the input
epoch times.
"""
assert len(epoch_array) > 0, "Input epoch_array must not be empty."
# assert np.issubdtype(epoch_array.dtype, np.integer), \
# "Input epoch_array must be an array of integers."

# Convert epoch times to datetime64 objects with an optional offset
return np.array(
[np.datetime64(int(epoch + delta), 's') for epoch in epoch_array]
)


def list_to_dict(list_of_str: list) -> dict:
"""
Converts a list of strings to a dictionary. The keys are the strings
Expand Down
41 changes: 0 additions & 41 deletions particula/util/tests/convert_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,44 +242,3 @@ def test_effective_refractive_index():
volume_zero=10,
volume_one=5,
) == (1.4572585227821824+0.3214931829339477j)


def test_datetime64_from_epoch_array():
"""Test the datetime64_from_epoch_array function."""
# Test with a simple example array
epoch_array = np.array([0, 1, 2, 3, 4])
expected_result = np.array(['1970-01-01T00:00:00', '1970-01-01T00:00:01',
'1970-01-01T00:00:02', '1970-01-01T00:00:03',
'1970-01-01T00:00:04'], dtype='datetime64[s]')
assert np.array_equal(
convert.datetime64_from_epoch_array(epoch_array),
expected_result
)

# Test with a non-zero delta
delta = 3600 # 1 hour in seconds
expected_result = np.array(['1970-01-01T01:00:00', '1970-01-01T01:00:01',
'1970-01-01T01:00:02', '1970-01-01T01:00:03',
'1970-01-01T01:00:04'], dtype='datetime64[s]')
assert np.array_equal(
convert.datetime64_from_epoch_array(epoch_array, delta),
expected_result
)

# Test with an empty array
empty_array = np.array([])
try:
convert.datetime64_from_epoch_array(empty_array)
assert False, \
"Function should raise an AssertionError for empty array."
except AssertionError:
pass

# Test with a non-integer array
float_array = np.array([0.0, 1.0, 2.0])
try:
convert.datetime64_from_epoch_array(float_array)
assert False, \
"Function should raise an AssertionError for non-integer array."
except AssertionError:
pass
43 changes: 43 additions & 0 deletions particula/util/tests/time_manage_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Test the time_manage module."""

import numpy as np
from particula.util import time_manage
from particula.util.time_manage import time_str_to_epoch


Expand Down Expand Up @@ -38,3 +40,44 @@ def test_time_str_to_epoch():
time,
time_format,
timezone_identifier) == expected


def test_datetime64_from_epoch_array():
"""Test the datetime64_from_epoch_array function."""
# Test with a simple example array
epoch_array = np.array([0, 1, 2, 3, 4])
expected_result = np.array(['1970-01-01T00:00:00', '1970-01-01T00:00:01',
'1970-01-01T00:00:02', '1970-01-01T00:00:03',
'1970-01-01T00:00:04'], dtype='datetime64[s]')
assert np.array_equal(
time_manage.datetime64_from_epoch_array(epoch_array),
expected_result
)

# Test with a non-zero delta
delta = 3600 # 1 hour in seconds
expected_result = np.array(['1970-01-01T01:00:00', '1970-01-01T01:00:01',
'1970-01-01T01:00:02', '1970-01-01T01:00:03',
'1970-01-01T01:00:04'], dtype='datetime64[s]')
assert np.array_equal(
time_manage.datetime64_from_epoch_array(epoch_array, delta),
expected_result
)

# Test with an empty array
empty_array = np.array([])
try:
time_manage.datetime64_from_epoch_array(empty_array)
assert False, \
"Function should raise an AssertionError for empty array."
except AssertionError:
pass

# Test with a non-integer array
float_array = np.array([0.0, 1.0, 2.0])
try:
time_manage.datetime64_from_epoch_array(float_array)
assert False, \
"Function should raise an AssertionError for non-integer array."
except AssertionError:
pass
Loading