Skip to content

Commit

Permalink
feat: Allow timeseries with single data column to be used without spe…
Browse files Browse the repository at this point in the history
…cifying column name (#203)
  • Loading branch information
Batch21 authored Jun 20, 2024
1 parent 70ba15d commit 6156cb8
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 21 deletions.
15 changes: 8 additions & 7 deletions pywr-schema/src/metric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ impl Metric {
}
Self::Timeseries(ts_ref) => {
let param_idx = match &ts_ref.columns {
TimeseriesColumns::Scenario(scenario) => {
Some(TimeseriesColumns::Scenario(scenario)) => {
args.timeseries
.load_df(network, ts_ref.name.as_ref(), args.domain, scenario.as_str())?
}
TimeseriesColumns::Column(col) => {
Some(TimeseriesColumns::Column(col)) => {
args.timeseries
.load_column(network, ts_ref.name.as_ref(), col.as_str())?
}
None => args.timeseries.load_single_column(network, ts_ref.name.as_ref())?,
};
Ok(MetricF64::ParameterValue(param_idx))
}
Expand Down Expand Up @@ -211,12 +212,12 @@ impl TryFromV1Parameter<ParameterValueV1> for Metric {
};

let cols = match (&t.column, &t.scenario) {
(Some(col), None) => TimeseriesColumns::Column(col.clone()),
(None, Some(scenario)) => TimeseriesColumns::Scenario(scenario.clone()),
(Some(col), None) => Some(TimeseriesColumns::Column(col.clone())),
(None, Some(scenario)) => Some(TimeseriesColumns::Scenario(scenario.clone())),
(Some(_), Some(_)) => {
return Err(ConversionError::AmbiguousColumnAndScenario(name.clone()))
}
(None, None) => return Err(ConversionError::MissingColumnOrScenario(name.clone())),
(None, None) => None,
};

Self::Timeseries(TimeseriesReference::new(name, cols))
Expand All @@ -238,11 +239,11 @@ pub enum TimeseriesColumns {
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, JsonSchema)]
pub struct TimeseriesReference {
name: String,
columns: TimeseriesColumns,
columns: Option<TimeseriesColumns>,
}

impl TimeseriesReference {
pub fn new(name: String, columns: TimeseriesColumns) -> Self {
pub fn new(name: String, columns: Option<TimeseriesColumns>) -> Self {
Self { name, columns }
}

Expand Down
6 changes: 3 additions & 3 deletions pywr-schema/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,10 @@ impl PywrNetwork {
};

let cols = match (&ts_ref.column, &ts_ref.scenario) {
(Some(col), None) => TimeseriesColumns::Column(col.clone()),
(None, Some(scenario)) => TimeseriesColumns::Scenario(scenario.clone()),
(Some(col), None) => Some(TimeseriesColumns::Column(col.clone())),
(None, Some(scenario)) => Some(TimeseriesColumns::Scenario(scenario.clone())),
(Some(_), Some(_)) => return,
(None, None) => return,
(None, None) => None,
};

*m = Metric::Timeseries(TimeseriesReference::new(name, cols));
Expand Down
6 changes: 1 addition & 5 deletions pywr-schema/src/test_models/timeseries.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,7 @@
"metrics": [
{
"type": "Timeseries",
"name": "inflow",
"columns": {
"type": "Column",
"name": "inflow1"
}
"name": "inflow"
},
{
"type": "Constant",
Expand Down
14 changes: 10 additions & 4 deletions pywr-schema/src/timeseries/align_and_resample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub fn align_and_resample(
df: DataFrame,
time_col: &str,
domain: &ModelDomain,
drop_time_col: bool,
) -> Result<DataFrame, TimeseriesError> {
// Ensure type of time column is datetime and that it is sorted
let sort_options = SortMultipleOptions::default()
Expand Down Expand Up @@ -78,12 +79,16 @@ pub fn align_and_resample(
Ordering::Equal => df,
};

let df = slice_end(df, time_col, domain)?;
let mut df = slice_end(df, time_col, domain)?;

if df.height() != domain.time().timesteps().len() {
return Err(TimeseriesError::DataFrameTimestepMismatch(name.to_string()));
}

if drop_time_col {
let _ = df.drop_in_place(time_col)?;
}

Ok(df)
}

Expand Down Expand Up @@ -141,7 +146,7 @@ mod tests {
)
.unwrap();

df = align_and_resample("test", df, "time", &domain).unwrap();
df = align_and_resample("test", df, "time", &domain, false).unwrap();

let expected_dates = Series::new(
"time",
Expand Down Expand Up @@ -195,7 +200,7 @@ mod tests {
)
.unwrap();

df = align_and_resample("test", df, "time", &domain).unwrap();
df = align_and_resample("test", df, "time", &domain, false).unwrap();

let expected_values = Series::new(
"values",
Expand Down Expand Up @@ -235,7 +240,7 @@ mod tests {
)
.unwrap();

df = align_and_resample("test", df, "time", &domain).unwrap();
df = align_and_resample("test", df, "time", &domain, false).unwrap();

let expected_values = Series::new("values", values);
let resampled_values = df.column("values").unwrap();
Expand All @@ -244,6 +249,7 @@ mod tests {
let expected_dates = Series::new("time", time)
.cast(&DataType::Datetime(TimeUnit::Nanoseconds, None))
.unwrap();

let resampled_dates = df.column("time").unwrap();
assert!(resampled_dates.equals(&expected_dates));
}
Expand Down
42 changes: 42 additions & 0 deletions pywr-schema/src/timeseries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub enum TimeseriesError {
DataFrameTimestepMismatch(String),
#[error("A timeseries dataframe with the name '{0}' already exists.")]
TimeseriesDataframeAlreadyExists(String),
#[error("The timeseries dataset '{0}' has more than one column of data so a column or scenario name must be provided for any reference")]
TimeseriesColumnOrScenarioRequired(String),
#[error("The timeseries dataset '{0}' has no columns")]
TimeseriesDataframeHasNoColumns(String),
#[error("Polars error: {0}")]
#[cfg(feature = "core")]
PolarsError(#[from] PolarsError),
Expand Down Expand Up @@ -149,6 +153,44 @@ impl LoadedTimeseriesCollection {
}
}

pub fn load_single_column(
&self,
network: &mut pywr_core::network::Network,
name: &str,
) -> Result<ParameterIndex<f64>, TimeseriesError> {
let df = self
.timeseries
.get(name)
.ok_or(TimeseriesError::TimeseriesNotFound(name.to_string()))?;

let cols = df.get_column_names();

if cols.len() > 1 {
return Err(TimeseriesError::TimeseriesColumnOrScenarioRequired(name.to_string()));
};

let col = cols.first().ok_or(TimeseriesError::ColumnNotFound {
col: "".to_string(),
name: name.to_string(),
})?;

let series = df.column(col)?;

let array = series.cast(&Float64)?.f64()?.to_ndarray()?.to_owned();
let name = format!("{}_{}", name, col);

match network.get_parameter_index_by_name(&name) {
Ok(idx) => Ok(idx),
Err(e) => match e {
PywrError::ParameterNotFound(_) => {
let p = Array1Parameter::new(&name, array, None);
Ok(network.add_parameter(Box::new(p))?)
}
_ => Err(TimeseriesError::PywrCore(e)),
},
}
}

pub fn load_df(
&self,
network: &mut pywr_core::network::Network,
Expand Down
4 changes: 2 additions & 2 deletions pywr-schema/src/timeseries/polars_dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ mod core {
};

df = match self.time_col {
Some(ref col) => align_and_resample(name, df, col, domain)?,
Some(ref col) => align_and_resample(name, df, col, domain, true)?,
None => {
// If a time col has not been provided assume it is the first column
let first_col = df.get_column_names()[0].to_string();
align_and_resample(name, df, first_col.as_str(), domain)?
align_and_resample(name, df, first_col.as_str(), domain, true)?
}
};

Expand Down

0 comments on commit 6156cb8

Please sign in to comment.