Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
702560b
ps: Adding new format of `TimeSeriesMetadataStore` (#84)
pesap May 8, 2025
968c1a8
perf: Changing backend json serialization/deserialization (#91)
pesap May 9, 2025
664f0c7
feat: Adding h5py backend and propagating changes (#94)
pesap May 15, 2025
acda183
feat: Adding new `DeterministicTimeSeries` (#95)
pesap Jul 16, 2025
f69e85f
Update formatting of initial_timestamp (#99)
jerrypotts Jul 17, 2025
cf2930e
fix: Adding fix from rebase
pesap Oct 3, 2025
72ed97b
deps: Updating dependencies
pesap Oct 3, 2025
42a544e
fix: Fixing some warnings on pytest
pesap Oct 3, 2025
ac2c692
ci: Updating workflows to be more simple
pesap Oct 3, 2025
10084e4
feat: Adding uv.lock since it is recommended by astral
pesap Oct 3, 2025
2ae0b42
feat: Adding `.show_components` method for system
pesap Oct 3, 2025
e1bb53c
bumping version to release candidate
pesap Oct 3, 2025
909309e
refactor: Removing DeterministicSingleTimeSeries in favor of a method…
pesap Oct 10, 2025
a1d8069
feat: Adding load system method (#108)
pesap Oct 10, 2025
ff9533f
bumpversion to new release candidate
pesap Oct 10, 2025
52fa23c
fix: loguru typo
pesap Nov 4, 2025
e62ed0a
Merge pull request #111 from NREL/fix/add-time-series-after-deserialize
daniel-thom Oct 18, 2025
80e9cd0
ci: Updating ci
pesap Nov 4, 2025
8ee8068
Merge branch 'main' into release/v1.0.0
pesap Nov 4, 2025
d5ea4fa
fix: unnecesary comments
pesap Nov 4, 2025
1f71028
fix: Adding h5py stubs.
pesap Nov 4, 2025
53b111c
fix: adding fixes for mypy and h5py
pesap Nov 4, 2025
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
65 changes: 37 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,48 +19,57 @@ jobs:
os: [ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[chronify,dev]"
- name: Run pytest with coverage
run: |
pytest -v --cov --cov-report=xml
- name: codecov
uses: codecov/[email protected]
if: ${{ matrix.os == env.DEFAULT_OS && matrix.python-version == env.DEFAULT_PYTHON }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: infrasys-tests
fail_ci_if_error: false
verbose: true
- uses: actions/checkout@v4
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install the project
run: uv sync --dev

- name: Run pytest with coverage
run: |
uv run pytest -v --cov --cov-report=xml

- name: codecov
uses: codecov/[email protected]
if: ${{ matrix.os == env.DEFAULT_OS && matrix.python-version == env.DEFAULT_PYTHON }}
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: infrasys-tests
fail_ci_if_error: false
verbose: true
mypy:
runs-on: ubuntu-latest
name: "mypy"

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Set up Python
uses: actions/setup-python@v5
uses: astral-sh/setup-uv@v6
with:
python-version-file: "pyproject.toml"
version: "latest"

- name: Installing dependencies
run: uv sync --dev

- name: Run mypy
run: |
uv run mypy --config-file=pyproject.toml --ignore-missing-imports src/
ruff:
runs-on: ubuntu-latest
name: "ruff"

steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
src: "./src"
version: "latest"

- name: Installing dependencies
run: uv sync --dev

- name: Run Ruff
run: uv run ruff check --output-format=github src/
27 changes: 13 additions & 14 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: select python version
uses: actions/setup-python@v5

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: "3.11"
- name: install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install ".[chronify,dev]"
- name: build documentation
run: |
cd docs
make clean
make html
- name: deploy
version: "latest"

- name: Install the project
run: uv sync --group docs

- name: Build Sphinx documentation
run: uv run sphinx-build docs/source/ docs/_build/

- name: Deploy on GitHub Pages
uses: peaceiris/[email protected]
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/_build/html
publish_dir: ./docs/_build/
force_orphan: true
full_commit_message: ${{ github.event.head_commit.message }}
99 changes: 86 additions & 13 deletions docs/explanation/time_series.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# Time Series

Infrastructure systems supports time series data expressed as a one-dimensional array of floats
using the class [SingleTimeSeries](#singe-time-series-api). Users must provide a `variable_name`
using the class {py:class}`infrasys.time_series_models.SingleTimeSeries`. Users must provide a `name`
that is typically the field of a component being modeled. For example, if the user has a time array
associated with the active power of a generator, they would assign
`variable_name = "active_power"`.
`name = "active_power"`.

Here is an example of how to create an instance of `SingleTimeSeries`:
Here is an example of how to create an instance of {py:class}`infrasys.time_series_models.SingleTimeSeries`:

```python
import random
time_series = SingleTimeSeries.from_array(
data=[random.random() for x in range(24)],
variable_name="active_power",
name="active_power",
initial_time=datetime(year=2030, month=1, day=1),
resolution=timedelta(hours=1),
)
Expand All @@ -23,20 +24,91 @@ there might be different profiles for different scenarios or model years.
```python
time_series = SingleTimeSeries.from_array(
data=[random.random() for x in range(24)],
variable_name="active_power",
name="active_power",
initial_time=datetime(year=2030, month=1, day=1),
resolution=timedelta(hours=1),
scenario="high",
model_year="2035",
)
```

## Deterministic Time Series

In addition to `SingleTimeSeries`, infrasys also supports deterministic time series,
which are used to represent forecasts or scenarios with a known future. There are two main types of
deterministic time series:

- {py:class}`infrasys.time_series_models.DeterministicTimeSeries`: Represents a time series where the data is explicitly stored as a 2D array, with each row representing a forecast window and each column representing a time step within that window.
- {py:class}`infrasys.time_series_models.DeterministicSingleTimeSeries`: Represents a deterministic forecast that wraps a `SingleTimeSeries`. Instead of storing the forecast data explicitly, it provides a view into the existing `SingleTimeSeries` at incrementing offsets. This is useful when you want to create a "perfect forecast" based on historical data or avoid data duplication when there are overlapping forecast windows.

### DeterministicTimeSeries

This class is used when you have explicit forecast data available. Each forecast window is stored as a row in a 2D array.

Example:

```python
import numpy as np
from datetime import datetime, timedelta
from infrasys.time_series_models import DeterministicTimeSeries
from infrasys.quantities import ActivePower

initial_time = datetime(year=2020, month=9, day=1)
resolution = timedelta(hours=1)
horizon = timedelta(hours=8) # 8 hours horizon (8 values per forecast)
interval = timedelta(hours=1) # 1 hour between forecasts
window_count = 3 # 3 forecast windows

# Create forecast data as a 2D array where:
# - Each row is a forecast window
# - Each column is a time step in the forecast horizon
forecast_data = [
[100.0, 101.0, 101.3, 90.0, 98.0, 87.0, 88.0, 67.0], # 2020-09-01T00 forecast
[101.0, 101.3, 99.0, 98.0, 88.9, 88.3, 67.1, 89.4], # 2020-09-01T01 forecast
[99.0, 67.0, 89.0, 99.9, 100.0, 101.0, 112.0, 101.3], # 2020-09-01T02 forecast
]

# Create the data with units
data = ActivePower(np.array(forecast_data), "watts")
name = "active_power_forecast"
ts = DeterministicTimeSeries.from_array(
data, name, initial_time, resolution, horizon, interval, window_count
)
```

### DeterministicSingleTimeSeries

This class is useful when you want to create a "perfect forecast" based on historical data or avoid data duplication. It wraps a `SingleTimeSeries` and provides a view into it at incrementing offsets.

Example:

```python
from datetime import datetime, timedelta
from infrasys.time_series_models import DeterministicSingleTimeSeries, SingleTimeSeries

initial_timestamp = datetime(year=2020, month=1, day=1)
name = "active_power"
ts = SingleTimeSeries.from_array(
data=range(8784),
name=name,
resolution=timedelta(hours=1),
initial_timestamp=initial_timestamp,
)
horizon = timedelta(hours=8)
interval = timedelta(hours=1)
ts_deterministic = DeterministicSingleTimeSeries.from_single_time_series(
ts, interval=interval, horizon=horizon
)
```

In this example, `ts_deterministic` provides a forecast for `active_power` by looking at the original `SingleTimeSeries` `ts` at different offsets determined by `interval` and `horizon`.

## Resolution

Infrastructure systems support two types of objects for passing the resolution:
:class:`datetime.timedelta` and :class:`dateutil.relativedelta.relativedelta`.
These types allow users to define durations with varying levels of granularity
and semantic meaning.
and semantic meaning.
While `timedelta` is best suited for precise, fixed-length
intervals (e.g., seconds, minutes, hours, days), `relativedelta` is more
appropriate for calendar-aware durations such as months or years, which do not
Expand All @@ -52,17 +124,18 @@ For example, a `timedelta` of 1 month will be converted to the ISO format string
`P1M` and a `timedelta` of 1 hour will be converted to `P0DT1H`.

## Behaviors

Users can customize time series behavior with these flags passed to the `System` constructor:

- `time_series_in_memory`: The `System` stores each array of data in an Arrow file by default. This
is a binary file that enables efficient storage and row access. Set this flag to store the data in
memory instead.
is a binary file that enables efficient storage and row access. Set this flag to store the data in
memory instead.
- `time_series_read_only`: The default behavior allows users to add and remove time series data.
Set this flag to disable mutation. That can be useful if you are de-serializing a system, won't be
changing it, and want to avoid copying the data.
Set this flag to disable mutation. That can be useful if you are de-serializing a system, won't be
changing it, and want to avoid copying the data.
- `time_series_directory`: The `System` stores time series data on the computer's tmp filesystem by
default. This filesystem may be of limited size. If your data will exceed that limit, such as what
is likely to happen on an HPC compute node, set this parameter to an alternate location (such as
`/tmp/scratch` on NREL's HPC systems).
default. This filesystem may be of limited size. If your data will exceed that limit, such as what
is likely to happen on an HPC compute node, set this parameter to an alternate location (such as
`/tmp/scratch` on NREL's HPC systems).

Refer to the [Time Series API](#time-series-api) for more information.
1 change: 1 addition & 0 deletions docs/how_tos/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
```{eval-rst}
.. _how-tos-page:
```

# How Tos

```{eval-rst}
Expand Down
16 changes: 9 additions & 7 deletions docs/how_tos/list_time_series.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ system.add_components(bus, gen)
length = 10
initial_time = datetime(year=2020, month=1, day=1)
timestamps = [initial_time + timedelta(hours=i) for i in range(length)]
variable_name = "active_power"
ts1 = SingleTimeSeries.from_time_array(np.random.rand(length), variable_name, timestamps)
ts2 = SingleTimeSeries.from_time_array(np.random.rand(length), variable_name, timestamps)
name = "active_power"
ts1 = SingleTimeSeries.from_time_array(np.random.rand(length), name, timestamps)
ts2 = SingleTimeSeries.from_time_array(np.random.rand(length), name, timestamps)
key1 = system.add_time_series(ts1, gen, scenario="low")
key2 = system.add_time_series(ts2, gen, scenario="high")

Expand All @@ -38,17 +38,19 @@ ts2_b = system.get_time_series_by_key(gen, key2)
for key in system.list_time_series_keys(gen):
print(f"{gen.label}: {key}")
```

```
SimpleGenerator.gen: variable_name='active_power' initial_time=datetime.datetime(2020, 1, 1, 0, 0) resolution=datetime.timedelta(seconds=3600) time_series_type=<class 'infrasys.time_series_models.SingleTimeSeries'> user_attributes={'scenario': 'high'} length=10
SimpleGenerator.gen: variable_name='active_power' initial_time=datetime.datetime(2020, 1, 1, 0, 0) resolution=datetime.timedelta(seconds=3600) time_series_type=<class 'infrasys.time_series_models.SingleTimeSeries'> user_attributes={'scenario': 'low'} length=10
SimpleGenerator.gen: name='active_power' initial_time=datetime.datetime(2020, 1, 1, 0, 0) resolution=datetime.timedelta(seconds=3600) time_series_type=<class 'infrasys.time_series_models.SingleTimeSeries'> user_attributes={'scenario': 'high'} length=10
SimpleGenerator.gen: name='active_power' initial_time=datetime.datetime(2020, 1, 1, 0, 0) resolution=datetime.timedelta(seconds=3600) time_series_type=<class 'infrasys.time_series_models.SingleTimeSeries'> user_attributes={'scenario': 'low'} length=10
```

You can also retrieve time series by specifying the parameters as shown here:

```python
system.time_series.get(gen, variable_name="active_power", scenario="high")
system.time_series.get(gen, name="active_power", scenario="high")
```

```
SingleTimeSeries(variable_name='active_power', normalization=None, data=array([0.29276233, 0.97400382, 0.76499075, 0.95080431, 0.61749027,
SingleTimeSeries(name='active_power', normalization=None, data=array([0.29276233, 0.97400382, 0.76499075, 0.95080431, 0.61749027,
0.73899945, 0.57877704, 0.3411286 , 0.80701393, 0.53051773]), resolution=datetime.timedelta(seconds=3600), initial_time=datetime.datetime(2020, 1, 1, 0, 0), length=10)
```
Loading