Skip to content

Commit 030ee94

Browse files
authored
Merge pull request #594 from daniel-thom/dt/speedup-transform-single-time-series
Speed up transform_single_time_series! for many SingleTimeSeries
2 parents 9af896f + 283cf4c commit 030ee94

2 files changed

Lines changed: 95 additions & 63 deletions

File tree

src/system_data.jl

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,15 @@ function _transform_single_time_series!(
707707
return
708708
end
709709

710+
# Every requested feature key-value pair must be present in the existing metadata's
711+
# features. This is an intentionally exact match, stricter than the SQL LIKE-based
712+
# partial feature match in list_metadata, which can over-match on substrings
713+
# (e.g., a request for key=1 matches a stored key=10) and on LIKE wildcard
714+
# characters in string values.
715+
_features_contain(existing_features, requested_features) =
716+
all(kv -> get(existing_features, kv.first, nothing) === kv.second,
717+
requested_features)
718+
710719
"""
711720
Check that all existing SingleTimeSeries can be converted to DeterministicSingleTimeSeries
712721
with the given horizon and interval.
@@ -724,12 +733,38 @@ function _check_transform_single_time_series(
724733
resolution::Union{Nothing, Dates.Period};
725734
skip_existing::Bool = false,
726735
)
736+
store = data.time_series_manager.metadata_store
727737
items = list_metadata_with_owner_uuid(
728-
data.time_series_manager.metadata_store,
738+
store,
729739
InfrastructureSystemsComponent;
730740
time_series_type = SingleTimeSeries,
731741
resolution = resolution,
732742
)
743+
744+
# The loop below runs once per SingleTimeSeries (potentially tens of thousands). To
745+
# avoid issuing one or more SQLite queries per iteration we build lookups up front:
746+
# - system forecast parameters depend only on (resolution, interval), so memoize them.
747+
# - fetch all existing Deterministic and DeterministicSingleTimeSeries metadata with
748+
# one query and index it by (owner_uuid, name, resolution). The per-series conflict
749+
# and skip_existing checks then run in memory against this lookup.
750+
forecast_params_cache =
751+
Dict{Tuple{Dates.Period, Dates.Period}, Union{Nothing, ForecastParameters}}()
752+
existing_forecasts =
753+
Dict{Tuple{Base.UUID, String, String}, Vector{TimeSeriesMetadata}}()
754+
for entry in list_metadata_with_owner_uuid(
755+
store,
756+
InfrastructureSystemsComponent;
757+
# Note: Deterministic matches both Deterministic and DeterministicSingleTimeSeries.
758+
time_series_type = Deterministic,
759+
)
760+
key = (
761+
entry.owner_uuid,
762+
get_name(entry.metadata),
763+
_serialize_period(get_resolution(entry.metadata)),
764+
)
765+
push!(get!(() -> TimeSeriesMetadata[], existing_forecasts, key), entry.metadata)
766+
end
767+
733768
components_with_params_and_metadata = NamedTuple[]
734769
for item in items
735770
params = _check_single_time_series_transformed_parameters(
@@ -738,61 +773,56 @@ function _check_transform_single_time_series(
738773
horizon,
739774
interval,
740775
)
741-
system_params = get_forecast_parameters(
742-
data.time_series_manager.metadata_store;
743-
resolution = params.resolution,
744-
interval = params.interval,
745-
)
776+
system_params = get!(forecast_params_cache, (params.resolution, params.interval)) do
777+
get_forecast_parameters(
778+
store;
779+
resolution = params.resolution,
780+
interval = params.interval,
781+
)
782+
end
746783
check_params_compatibility(system_params, params)
747784
component = get_component(data, item.owner_uuid)
748785

786+
ts_name = get_name(item.metadata)
787+
ts_resolution = get_resolution(item.metadata)
788+
ts_features = get_features(item.metadata)
789+
existing = get(
790+
existing_forecasts,
791+
(item.owner_uuid, ts_name, _serialize_period(ts_resolution)),
792+
nothing,
793+
)
794+
749795
# We do not allow a component to have both Deterministic and
750796
# DeterministicSingleTimeSeries with the same parameters.
751797
# The user might be calling this function because some components are missing
752798
# Deterministic forecasts. If other components already have Deterministic forecasts,
753799
# this check will fail.
754800
# transform_single_time_series! cannot be called at the component level.
755-
# Note: has_metadata with Deterministic matches both Deterministic and
756-
# DeterministicSingleTimeSeries. Use list_metadata and filter to check only for
757-
# actual Deterministic forecasts.
758-
ts_name = get_name(item.metadata)
759-
ts_resolution = get_resolution(item.metadata)
760-
ts_features = get_features(item.metadata)
761-
ts_features_symbols = Dict{Symbol, Any}(Symbol(k) => v for (k, v) in ts_features)
762-
existing_det = list_metadata(
763-
data.time_series_manager.metadata_store,
764-
component;
765-
time_series_type = Deterministic,
766-
name = ts_name,
767-
resolution = ts_resolution,
768-
ts_features_symbols...,
769-
)
770-
if any(m -> get_time_series_type(m) === Deterministic, existing_det)
771-
throw(
772-
ConflictingInputsError(
773-
"Cannot transform SingleTimeSeries to DeterministicSingleTimeSeries: " *
774-
"A Deterministic forecast already exists for component $(summary(component)) " *
775-
"with name='$ts_name', resolution=$ts_resolution, and features=$ts_features",
776-
),
801+
if !isnothing(existing)
802+
if any(
803+
m ->
804+
get_time_series_type(m) === Deterministic &&
805+
_features_contain(get_features(m), ts_features),
806+
existing,
777807
)
778-
end
808+
throw(
809+
ConflictingInputsError(
810+
"Cannot transform SingleTimeSeries to DeterministicSingleTimeSeries: " *
811+
"A Deterministic forecast already exists for component $(summary(component)) " *
812+
"with name='$ts_name', resolution=$ts_resolution, and features=$ts_features",
813+
),
814+
)
815+
end
779816

780-
# If skip_existing is true, skip SingleTimeSeries entries that already have a
781-
# DeterministicSingleTimeSeries with the same name, resolution, features,
782-
# horizon, and interval.
783-
if skip_existing
784-
existing = list_metadata(
785-
data.time_series_manager.metadata_store,
786-
component;
787-
time_series_type = DeterministicSingleTimeSeries,
788-
name = ts_name,
789-
resolution = ts_resolution,
790-
ts_features_symbols...,
791-
)
792-
if any(
817+
# If skip_existing is true, skip SingleTimeSeries entries that already have a
818+
# DeterministicSingleTimeSeries with the same name, resolution, features,
819+
# horizon, and interval.
820+
if skip_existing && any(
793821
m ->
794-
get_horizon(m) == params.horizon &&
795-
get_interval(m) == params.interval,
822+
get_time_series_type(m) === DeterministicSingleTimeSeries &&
823+
get_horizon(m) == params.horizon &&
824+
get_interval(m) == params.interval &&
825+
_features_contain(get_features(m), ts_features),
796826
existing,
797827
)
798828
continue

src/time_series_metadata_store.jl

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
const ASSOCIATIONS_TABLE_NAME = "time_series_associations"
22
const METADATA_TABLE_NAME = "time_series_metadata"
33
const KEY_VALUE_TABLE_NAME = "key_value_store"
4+
# Column order must match the tuple returned by _create_row and the associations schema.
5+
const ASSOCIATIONS_TABLE_COLUMNS = (
6+
"id",
7+
"time_series_uuid",
8+
"time_series_type",
9+
"initial_timestamp",
10+
"resolution",
11+
"horizon",
12+
"interval",
13+
"window_count",
14+
"length",
15+
"name",
16+
"owner_uuid",
17+
"owner_type",
18+
"owner_category",
19+
"features",
20+
"scaling_factor_multiplier",
21+
"metadata_uuid",
22+
"units",
23+
)
424
const DB_FILENAME = "time_series_metadata.db"
525
# This version is also used in the Python package infrasys.
626
const TS_METADATA_FORMAT_VERSION = "1.1.0"
@@ -328,25 +348,7 @@ function _add_migrated_rows!(store::TimeSeriesMetadataStore, rows)
328348
_add_rows!(
329349
store.db,
330350
rows,
331-
(
332-
"id",
333-
"time_series_uuid",
334-
"time_series_type",
335-
"initial_timestamp",
336-
"resolution",
337-
"horizon",
338-
"interval",
339-
"window_count",
340-
"length",
341-
"name",
342-
"owner_uuid",
343-
"owner_type",
344-
"owner_category",
345-
"features",
346-
"scaling_factor_multiplier",
347-
"metadata_uuid",
348-
"units",
349-
),
351+
ASSOCIATIONS_TABLE_COLUMNS,
350352
ASSOCIATIONS_TABLE_NAME,
351353
)
352354
_create_key_value_table!(store)

0 commit comments

Comments
 (0)