-
Notifications
You must be signed in to change notification settings - Fork 4
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
WIP: feat: switch from time-rs to chrono and implement non-daily timesteps #115
Conversation
@jetuk would be good to get some feedback on this. One potential change I've just noticed is that the virtual storage state will need updating to store timestep length, otherwise there will be no way to know the amount of flow to recover for each timestep. |
I'm not sure I understand? The current implementation uses |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've had a quick read through and left some comments.
@@ -76,13 +86,16 @@ impl Parameter for MonthlyProfileParameter { | |||
let v = match &self.interp_day { | |||
Some(interp_day) => match interp_day { | |||
MonthlyInterpDay::First => { | |||
let next_month = (timestep.date.month() % 12) + 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this correct? .month()
is 1-based here?
// Returns the zero-based next month
let next_month0 = (timestep.date.month0() + 1) % 12;
let last_value = self.values[next_month0 as usize];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chrono::Month
?
|
||
interpolate_first(×tep.date, first_value, last_value) | ||
} | ||
MonthlyInterpDay::Last => { | ||
let first_value = self.values[timestep.date.month().previous() as usize - 1]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The time API is much nicer here.
start: Date, | ||
end: Date, | ||
timestep: Duration, | ||
start: NaiveDateTime, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a concious decision to add time into the time-stepping?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, to make sub-daily timesteps possible. If we didn't want to allow these at this point we could revert to a NaiveDate
pywr-schema/src/model.rs
Outdated
@@ -48,20 +49,40 @@ impl From<pywr_v1_schema::model::Timestep> for Timestep { | |||
} | |||
} | |||
|
|||
#[derive(serde::Deserialize, serde::Serialize, Clone, Copy)] | |||
pub struct DateTimeComponents { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because you can't deserialise a string without a time component if the type is a NaiveDateTime
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, at a minimum I think the string has to have hour and minute components
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could leave it as a date that automatically gets added 00:00:00. Or make a untagged enum for a Date string or this date-time struct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've gone with an untagged enum which seems to work.
In |
I understand. It seems like the history would need a method to pop off mean
flow over a period equal to the current time-step duration. And yes,
therefore it would need to keep a record of the durations associated with
each history entry.
I think generally this will be ok as it is because time-steps tend to be
similar in duration if bit identical. Therefore, note it as an issue and
we'll fix it in another PR.
…On Sat, 17 Feb 2024, 10:52 James Batchelor, ***@***.***> wrote:
One potential change I've just noticed is that the virtual storage state
will need updating to store timestep length, otherwise there will be no way
to know the amount of flow to recover for each timestep.
I'm not sure I understand? The current implementation uses Timestep.days()
the same as the regular storage state?
In recover_virtual_storage_last_historical_flow the current timestep is
used but its length might be different to the timestep when the flow added
to the storage state?
—
Reply to this email directly, view it on GitHub
<#115 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABDEF3TMUAI3V26WW4S72ULYUCDXXAVCNFSM6AAAAABDKMMTA6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSNBZHEZTINJTGM>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
I wonder if it would be beneficial to have two different |
This wraps the Chrono TimeDelta type and provides a fractional_days method that can be used for metric aggregation.
@jetuk I think this is ready for you to take another look |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some quick comments on the latest changes. I'll do a separate review of the whole thing.
} | ||
} | ||
|
||
impl PywrDuration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to add some simple doc strings to these methods and the struct itself.
pywr-core/src/timestep.rs
Outdated
} | ||
|
||
pub fn whole_days(&self) -> Option<i64> { | ||
if self.fractional_days() % 1.0 == 0.0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this pass clippy checks of comparing against floating point values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get a warning locally. Is there going to be an issue with floating point precision here? Might be better to use the float_cmp
crate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was possible expecting clippy to show this: https://rust-lang.github.io/rust-clippy/master/index.html#/float_cmp_const
However, it is part of the "restriction" group which means it should be considered case-by-case. The question is whether there's any reasonable output of fractional_days()
which we expect to be treated as a whole day, but would not be by this test? Leap seconds? Are those still a thing?
If there's not already just add a test for this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Batch21 I did a bit of reading here. I think modulo on floats is a bit weird sometimes. It might be better to do the modulo on the num_seconds()
as a i64
.
Something like this might be better?
const SECS_IN_DAY: i64 = 60 * 60 * 24;
pub fn whole_days(&self) -> Option<i64> {
if self.0.num_seconds() % SECS_IN_DAY == 0 {
Some(self.0.num_days())
} else {
None
}
}
pub fn fractional_days(&self) -> f64 {
self.0.num_seconds() as f64 / SECS_IN_DAY as f64
}
@@ -47,7 +48,7 @@ impl AggregationFrequency { | |||
let mut sub_values = Vec::new(); | |||
|
|||
let mut current_date = value.start; | |||
let end_date = value.start + value.duration; | |||
let end_date = value.start + *value.duration.time_delta(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One alternative here would be to implement Add
for PywrDuration
with rhs = NaiveDateTime
pub fn from(timestepper: Timestepper, scenario_collection: ScenarioGroupCollection) -> Self { | ||
Self { | ||
time: timestepper.into(), | ||
pub fn from(timestepper: Timestepper, scenario_collection: ScenarioGroupCollection) -> Result<Self, PywrError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this to try_from
@@ -5,8 +5,8 @@ use crate::state::{ParameterState, State}; | |||
use crate::timestep::Timestep; | |||
use crate::PywrError; | |||
use std::any::Any; | |||
use time::util::days_in_year_month; | |||
use time::Date; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove empty line
pywr-core/src/parameters/py.rs
Outdated
@@ -120,7 +121,7 @@ impl Parameter for PyParameter { | |||
py, | |||
timestep.date.year(), | |||
timestep.date.month() as u8, | |||
timestep.date.day(), | |||
timestep.date.day() as u8, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use the chrono
feature in pyo3 to pass the date directly as an argument.
pywr-core/src/parameters/py.rs
Outdated
@@ -154,7 +155,7 @@ impl Parameter for PyParameter { | |||
py, | |||
timestep.date.year(), | |||
timestep.date.month() as u8, | |||
timestep.date.day(), | |||
timestep.date.day() as u8, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above.
month: ts.date.month().into(), | ||
day: ts.date.day(), | ||
month: ts.date.month() as u8, | ||
day: ts.date.day() as u8, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to support saving the HMS data in the HDF file?
pywr-core/src/test_utils.rs
Outdated
} | ||
|
||
pub fn default_time_domain() -> TimeDomain { | ||
default_timestepper().into() | ||
TimeDomain::try_from(default_timestepper()).unwrap() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default_timestepper().try_into().unwrap()
should also work here and not require the TimeDomain
type to written twice.
use pyo3::prelude::*; | ||
use std::ops::Add; | ||
use time::{Date, Duration}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove empty line.
pywr-core/src/timestep.rs
Outdated
timesteps.push(current); | ||
current = next; | ||
} | ||
timesteps | ||
} | ||
|
||
fn generate_timesteps_from_frequency(&self, frequency: String) -> Result<Vec<Timestep>, PywrError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a docstring here. Also it would be more idiomatic to use frequency: &str
.
pywr-core/src/virtual_storage.rs
Outdated
@@ -352,7 +375,8 @@ mod tests { | |||
let recorder = AssertionFnRecorder::new("link-1-flow", Metric::NodeOutFlow(idx), expected, None, None); | |||
network.add_recorder(Box::new(recorder)).unwrap(); | |||
|
|||
let model = Model::new(default_timestepper().into(), network); | |||
let domain = ModelDomain::try_from(default_timestepper()).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let domain = default_timestepper().try_into().unwrap();
is slightly shorter.
pywr-python/src/lib.rs
Outdated
let start = | ||
Date::from_calendar_date(start.get_year(), start.get_month().try_into().unwrap(), start.get_day()).unwrap(); | ||
let end = Date::from_calendar_date(end.get_year(), end.get_month().try_into().unwrap(), end.get_day()).unwrap(); | ||
let start = DateType::Date( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we support datetime in this API as well?
@jetuk I think the latest commits address all your comments |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Good update!
No description provided.