use cobre_core::EntityId;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use std::fs::File;
use std::path::Path;
use crate::LoadError;
use crate::parquet_helpers::{
extract_optional_float64, extract_optional_int32, extract_required_int32,
};
use super::bounds::{optional_f64, optional_i32, validate_optional_finite};
#[derive(Debug, Clone, PartialEq)]
pub struct HydroUnitGroupBoundsRow {
pub hydro_id: EntityId,
pub hydro_unit_group_id: EntityId,
pub stage_id: i32,
pub min_turbined_m3s: Option<f64>,
pub max_turbined_m3s: Option<f64>,
pub min_generation_mw: Option<f64>,
pub max_generation_mw: Option<f64>,
pub block_id: Option<i32>,
}
pub fn parse_hydro_unit_group_bounds(
path: &Path,
) -> Result<Vec<HydroUnitGroupBoundsRow>, LoadError> {
let file = File::open(path).map_err(|e| LoadError::io(path, e))?;
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
.map_err(|e| LoadError::parse(path, e.to_string()))?;
let reader = builder
.build()
.map_err(|e| LoadError::parse(path, e.to_string()))?;
let mut rows: Vec<HydroUnitGroupBoundsRow> = Vec::new();
for batch_result in reader {
let batch = batch_result.map_err(|e| LoadError::parse(path, e.to_string()))?;
let hydro_id_col = extract_required_int32(&batch, "hydro_id", path)?;
let group_id_col = extract_required_int32(&batch, "hydro_unit_group_id", path)?;
let stage_id_col = extract_required_int32(&batch, "stage_id", path)?;
let min_turbined_col = extract_optional_float64(&batch, "min_turbined_m3s", path)?;
let max_turbined_col = extract_optional_float64(&batch, "max_turbined_m3s", path)?;
let min_gen_col = extract_optional_float64(&batch, "min_generation_mw", path)?;
let max_gen_col = extract_optional_float64(&batch, "max_generation_mw", path)?;
let block_id_col = extract_optional_int32(&batch, "block_id", path)?;
let n = batch.num_rows();
let base_idx = rows.len();
rows.reserve(n);
for i in 0..n {
let row_idx = base_idx + i;
let hydro_id = EntityId::from(hydro_id_col.value(i));
let hydro_unit_group_id = EntityId::from(group_id_col.value(i));
let stage_id = stage_id_col.value(i);
let min_turbined_m3s = optional_f64(min_turbined_col, i);
let max_turbined_m3s = optional_f64(max_turbined_col, i);
let min_generation_mw = optional_f64(min_gen_col, i);
let max_generation_mw = optional_f64(max_gen_col, i);
let block_id = optional_i32(block_id_col, i);
for (value, column) in [
(min_turbined_m3s, "min_turbined_m3s"),
(max_turbined_m3s, "max_turbined_m3s"),
(min_generation_mw, "min_generation_mw"),
(max_generation_mw, "max_generation_mw"),
] {
validate_optional_finite(value, "hydro_unit_group_bounds", row_idx, column, path)?;
}
rows.push(HydroUnitGroupBoundsRow {
hydro_id,
hydro_unit_group_id,
stage_id,
min_turbined_m3s,
max_turbined_m3s,
min_generation_mw,
max_generation_mw,
block_id,
});
}
}
rows.sort_by_key(|r| {
(
r.hydro_id.0,
r.hydro_unit_group_id.0,
r.stage_id,
r.block_id,
)
});
Ok(rows)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::float_cmp,
clippy::panic
)]
mod tests {
use super::*;
use arrow::array::{Float64Array, Int32Array};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use parquet::arrow::ArrowWriter;
use std::sync::Arc;
use tempfile::NamedTempFile;
fn write_parquet(batch: &RecordBatch) -> NamedTempFile {
let tmp = NamedTempFile::new().expect("tempfile");
let mut writer = ArrowWriter::try_new(tmp.reopen().expect("reopen"), batch.schema(), None)
.expect("ArrowWriter");
writer.write(batch).expect("write batch");
writer.close().expect("close writer");
tmp
}
#[test]
fn parse_hydro_unit_group_bounds_sorts_by_full_key() {
let schema = Arc::new(Schema::new(vec![
Field::new("hydro_id", DataType::Int32, false),
Field::new("hydro_unit_group_id", DataType::Int32, false),
Field::new("stage_id", DataType::Int32, false),
Field::new("min_turbined_m3s", DataType::Float64, true),
Field::new("max_turbined_m3s", DataType::Float64, true),
Field::new("min_generation_mw", DataType::Float64, true),
Field::new("max_generation_mw", DataType::Float64, true),
Field::new("block_id", DataType::Int32, true),
]));
let batch = RecordBatch::try_new(
Arc::clone(&schema),
vec![
Arc::new(Int32Array::from(vec![1, 1, 1, 0])),
Arc::new(Int32Array::from(vec![5, 2, 5, 9])),
Arc::new(Int32Array::from(vec![2, 2, 2, 5])),
Arc::new(Float64Array::from(vec![
Some(10.0),
Some(20.0),
None,
Some(1.0),
])),
Arc::new(Float64Array::from(vec![None, None, None, None])),
Arc::new(Float64Array::from(vec![None, None, None, None])),
Arc::new(Float64Array::from(vec![None, None, Some(99.0), None])),
Arc::new(Int32Array::from(vec![None, None, Some(3), None])),
],
)
.expect("valid batch");
let tmp = write_parquet(&batch);
let rows = parse_hydro_unit_group_bounds(tmp.path()).expect("valid file");
assert_eq!(rows.len(), 4);
assert_eq!(rows[0].hydro_id, EntityId::from(0));
assert_eq!(rows[0].hydro_unit_group_id, EntityId::from(9));
assert_eq!(rows[0].stage_id, 5);
assert_eq!(rows[1].hydro_id, EntityId::from(1));
assert_eq!(rows[1].hydro_unit_group_id, EntityId::from(2));
assert_eq!(rows[1].stage_id, 2);
assert!(rows[1].block_id.is_none());
assert_eq!(rows[2].hydro_id, EntityId::from(1));
assert_eq!(rows[2].hydro_unit_group_id, EntityId::from(5));
assert_eq!(rows[2].stage_id, 2);
assert!(rows[2].block_id.is_none());
assert_eq!(rows[3].hydro_id, EntityId::from(1));
assert_eq!(rows[3].hydro_unit_group_id, EntityId::from(5));
assert_eq!(rows[3].stage_id, 2);
assert_eq!(rows[3].block_id, Some(3));
assert_eq!(rows[3].max_generation_mw, Some(99.0));
}
#[test]
fn parse_hydro_unit_group_bounds_rejects_non_finite() {
let schema = Arc::new(Schema::new(vec![
Field::new("hydro_id", DataType::Int32, false),
Field::new("hydro_unit_group_id", DataType::Int32, false),
Field::new("stage_id", DataType::Int32, false),
Field::new("max_generation_mw", DataType::Float64, true),
]));
let batch = RecordBatch::try_new(
schema,
vec![
Arc::new(Int32Array::from(vec![1, 1])),
Arc::new(Int32Array::from(vec![1, 1])),
Arc::new(Int32Array::from(vec![0, 1])),
Arc::new(Float64Array::from(vec![Some(10.0), Some(f64::NAN)])),
],
)
.expect("valid batch");
let tmp = write_parquet(&batch);
let err = parse_hydro_unit_group_bounds(tmp.path()).unwrap_err();
match &err {
LoadError::SchemaError { field, .. } => {
assert_eq!(field, "hydro_unit_group_bounds[1].max_generation_mw");
}
other => panic!("expected SchemaError, got: {other:?}"),
}
}
#[test]
fn parse_hydro_unit_group_bounds_treats_absent_columns_as_none() {
let schema = Arc::new(Schema::new(vec![
Field::new("hydro_id", DataType::Int32, false),
Field::new("hydro_unit_group_id", DataType::Int32, false),
Field::new("stage_id", DataType::Int32, false),
]));
let batch = RecordBatch::try_new(
schema,
vec![
Arc::new(Int32Array::from(vec![1])),
Arc::new(Int32Array::from(vec![2])),
Arc::new(Int32Array::from(vec![0])),
],
)
.expect("valid batch");
let tmp = write_parquet(&batch);
let rows = parse_hydro_unit_group_bounds(tmp.path()).expect("valid file");
assert_eq!(rows.len(), 1);
assert!(rows[0].min_turbined_m3s.is_none());
assert!(rows[0].max_turbined_m3s.is_none());
assert!(rows[0].min_generation_mw.is_none());
assert!(rows[0].max_generation_mw.is_none());
assert!(rows[0].block_id.is_none());
}
}