diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml new file mode 100644 index 00000000..300789f5 --- /dev/null +++ b/.github/workflows/deploy-book.yml @@ -0,0 +1,42 @@ +name: Deploy book +on: + # Deploy the book when the Rust (Linux) workflow completes on main + workflow_run: + workflows: ["Rust (Linux)"] + branches: [main] + types: + - completed + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: write # To push a branch + pages: write # To push to a GitHub Pages site + id-token: write # To update the deployment status + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install latest mdbook + run: | + tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') + url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" + mkdir mdbook + curl -sSL $url | tar -xz --directory=./mdbook + echo `pwd`/mdbook >> $GITHUB_PATH + - name: Build Book + run: | + # This assumes your book is in the root of your repository. + # Just add a `cd` here if you need to change to another directory. + mdbook build ./pywr-book + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: './pywr-book/book' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/Cargo.toml b/Cargo.toml index bb55079c..67357778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ default-members = [ "pywr-core", "pywr-schema", "pywr-cli", - "pywr-python", + # "pywr-python", ] @@ -35,14 +35,14 @@ opt-level = 3 # fast and small wasm serde = { version = "1", features = ["derive"] } serde_json = "1.0" thiserror = "1.0.25" -time = { version = "0.3", features = ["serde", "serde-well-known", "serde-human-readable", "macros"] } num = "0.4.0" ndarray = "0.15.3" polars = { version = "0.37.0", features = ["lazy", "rows", "ndarray"] } pyo3-polars = "0.11.1" -pyo3 = { version = "0.20.2" } -tracing = "0.1" +pyo3 = { version = "0.20.2", default-features = false } +pyo3-log = "0.9.0" +tracing = { version = "0.1", features = ["log"] } csv = "1.1" -hdf5 = { version="0.8.1" } -hdf5-sys = { version="0.8.1", features=["static"] } -pywr-v1-schema = { git = "https://github.com/pywr/pywr-schema/", tag="v0.9.0", package = "pywr-schema" } +hdf5 = { git = "https://github.com/aldanor/hdf5-rust.git", package = "hdf5", features = ["static", "zlib"] } +pywr-v1-schema = { git = "https://github.com/pywr/pywr-schema/", tag = "v0.11.0", package = "pywr-schema" } +chrono = { version = "0.4.34" } diff --git a/clp-sys/Cargo.toml b/clp-sys/Cargo.toml index a17fe7a7..2281569b 100644 --- a/clp-sys/Cargo.toml +++ b/clp-sys/Cargo.toml @@ -2,7 +2,11 @@ name = "clp-sys" version = "0.1.0" authors = ["James Tomlinson "] -edition = "2018" +edition = "2021" +description = "Low-level bindings to COIN-OR CLP (Coin-or linear programming)." +license = "MIT OR Apache-2.0" +repository = "https://github.com/pywr/pywr-next/" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/clp-sys/build.rs b/clp-sys/build.rs index 3f9a8ab6..1e2c5336 100644 --- a/clp-sys/build.rs +++ b/clp-sys/build.rs @@ -23,23 +23,28 @@ fn make_builder() -> cc::Build { .to_owned(); if target.contains("msvc") { - builder.flag("-EHsc").flag_if_supported("-std:c++11"); + builder.flag("-EHsc"); + // Flag required for macros __cplusplus to work correctly. + // See: https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ + builder.flag("/Zc:__cplusplus"); + builder.flag("/std:c++14"); } else { - builder.flag("-std=c++11").flag("-w"); + builder.flag("-std=c++11"); + builder.flag("-w"); } builder } fn main() { - const COIN_UTILS_SRC: &str = "vendor/CoinUtils/src"; - const COIN_CLP_SRC: &str = "vendor/Clp/src"; + const COIN_UTILS_SRC: &str = "vendor/CoinUtils/CoinUtils/src"; + const COIN_CLP_SRC: &str = "vendor/Clp/Clp/src"; // Compile CoinUtils let mut builder = make_builder(); builder - .flag(&*format!("-I{}", COIN_UTILS_SRC)) + .flag(&format!("-I{}", COIN_UTILS_SRC)) .file(format!("{}/CoinAlloc.cpp", COIN_UTILS_SRC)) .file(format!("{}/CoinBuild.cpp", COIN_UTILS_SRC)) .file(format!("{}/CoinDenseFactorization.cpp", COIN_UTILS_SRC)) @@ -104,8 +109,8 @@ fn main() { let mut builder = make_builder(); builder - .flag(&*format!("-I{}", COIN_UTILS_SRC)) - .flag(&*format!("-I{}", COIN_CLP_SRC)) + .flag(&format!("-I{}", COIN_UTILS_SRC)) + .flag(&format!("-I{}", COIN_CLP_SRC)) .file(format!("{}/ClpCholeskyBase.cpp", COIN_CLP_SRC)) .file(format!("{}/ClpCholeskyDense.cpp", COIN_CLP_SRC)) .file(format!("{}/ClpCholeskyPardiso.cpp", COIN_CLP_SRC)) diff --git a/clp-sys/src/bindings.rs b/clp-sys/src/bindings.rs index e873bc67..f6919306 100644 --- a/clp-sys/src/bindings.rs +++ b/clp-sys/src/bindings.rs @@ -1,236 +1,9 @@ -/* automatically generated by rust-bindgen 0.57.0 */ +/* automatically generated by rust-bindgen 0.69.4 */ -pub const CLP_VERSION: &'static [u8; 6usize] = b"trunk\0"; -pub const CLP_VERSION_MAJOR: u32 = 9999; -pub const CLP_VERSION_MINOR: u32 = 9999; -pub const CLP_VERSION_RELEASE: u32 = 9999; -pub const COINUTILS_VERSION: &'static [u8; 7usize] = b"master\0"; -pub const COINUTILS_VERSION_MAJOR: u32 = 9999; -pub const COINUTILS_VERSION_MINOR: u32 = 9999; -pub const COINUTILS_VERSION_RELEASE: u32 = 9999; -pub const COINUTILS_BIGINDEX_IS_INT: u32 = 1; -pub const COINUTILS_CPLUSPLUS11: u32 = 1; -pub const COINUTILS_HAS_CSTDINT: u32 = 1; -pub const COINUTILS_HAS_STDINT_H: u32 = 1; -pub const _STDINT_H: u32 = 1; -pub const _FEATURES_H: u32 = 1; -pub const _DEFAULT_SOURCE: u32 = 1; -pub const __GLIBC_USE_ISOC2X: u32 = 0; -pub const __USE_ISOC11: u32 = 1; -pub const __USE_ISOC99: u32 = 1; -pub const __USE_ISOC95: u32 = 1; -pub const __USE_POSIX_IMPLICITLY: u32 = 1; -pub const _POSIX_SOURCE: u32 = 1; -pub const _POSIX_C_SOURCE: u32 = 200809; -pub const __USE_POSIX: u32 = 1; -pub const __USE_POSIX2: u32 = 1; -pub const __USE_POSIX199309: u32 = 1; -pub const __USE_POSIX199506: u32 = 1; -pub const __USE_XOPEN2K: u32 = 1; -pub const __USE_XOPEN2K8: u32 = 1; -pub const _ATFILE_SOURCE: u32 = 1; -pub const __USE_MISC: u32 = 1; -pub const __USE_ATFILE: u32 = 1; -pub const __USE_FORTIFY_LEVEL: u32 = 0; -pub const __GLIBC_USE_DEPRECATED_GETS: u32 = 0; -pub const __GLIBC_USE_DEPRECATED_SCANF: u32 = 0; -pub const _STDC_PREDEF_H: u32 = 1; -pub const __STDC_IEC_559__: u32 = 1; -pub const __STDC_IEC_559_COMPLEX__: u32 = 1; -pub const __STDC_ISO_10646__: u32 = 201706; -pub const __GNU_LIBRARY__: u32 = 6; -pub const __GLIBC__: u32 = 2; -pub const __GLIBC_MINOR__: u32 = 32; -pub const _SYS_CDEFS_H: u32 = 1; -pub const __glibc_c99_flexarr_available: u32 = 1; -pub const __WORDSIZE: u32 = 64; -pub const __WORDSIZE_TIME64_COMPAT32: u32 = 1; -pub const __SYSCALL_WORDSIZE: u32 = 64; -pub const __LDOUBLE_REDIRECTS_TO_FLOAT128_ABI: u32 = 0; -pub const __HAVE_GENERIC_SELECTION: u32 = 1; -pub const __GLIBC_USE_LIB_EXT2: u32 = 0; -pub const __GLIBC_USE_IEC_60559_BFP_EXT: u32 = 0; -pub const __GLIBC_USE_IEC_60559_BFP_EXT_C2X: u32 = 0; -pub const __GLIBC_USE_IEC_60559_FUNCS_EXT: u32 = 0; -pub const __GLIBC_USE_IEC_60559_FUNCS_EXT_C2X: u32 = 0; -pub const __GLIBC_USE_IEC_60559_TYPES_EXT: u32 = 0; -pub const _BITS_TYPES_H: u32 = 1; -pub const __TIMESIZE: u32 = 64; -pub const _BITS_TYPESIZES_H: u32 = 1; -pub const __OFF_T_MATCHES_OFF64_T: u32 = 1; -pub const __INO_T_MATCHES_INO64_T: u32 = 1; -pub const __RLIM_T_MATCHES_RLIM64_T: u32 = 1; -pub const __STATFS_MATCHES_STATFS64: u32 = 1; -pub const __KERNEL_OLD_TIMEVAL_MATCHES_TIMEVAL64: u32 = 1; -pub const __FD_SETSIZE: u32 = 1024; -pub const _BITS_TIME64_H: u32 = 1; -pub const _BITS_WCHAR_H: u32 = 1; -pub const _BITS_STDINT_INTN_H: u32 = 1; -pub const _BITS_STDINT_UINTN_H: u32 = 1; -pub const INT8_MIN: i32 = -128; -pub const INT16_MIN: i32 = -32768; -pub const INT32_MIN: i32 = -2147483648; -pub const INT8_MAX: u32 = 127; -pub const INT16_MAX: u32 = 32767; -pub const INT32_MAX: u32 = 2147483647; -pub const UINT8_MAX: u32 = 255; -pub const UINT16_MAX: u32 = 65535; -pub const UINT32_MAX: u32 = 4294967295; -pub const INT_LEAST8_MIN: i32 = -128; -pub const INT_LEAST16_MIN: i32 = -32768; -pub const INT_LEAST32_MIN: i32 = -2147483648; -pub const INT_LEAST8_MAX: u32 = 127; -pub const INT_LEAST16_MAX: u32 = 32767; -pub const INT_LEAST32_MAX: u32 = 2147483647; -pub const UINT_LEAST8_MAX: u32 = 255; -pub const UINT_LEAST16_MAX: u32 = 65535; -pub const UINT_LEAST32_MAX: u32 = 4294967295; -pub const INT_FAST8_MIN: i32 = -128; -pub const INT_FAST16_MIN: i64 = -9223372036854775808; -pub const INT_FAST32_MIN: i64 = -9223372036854775808; -pub const INT_FAST8_MAX: u32 = 127; -pub const INT_FAST16_MAX: u64 = 9223372036854775807; -pub const INT_FAST32_MAX: u64 = 9223372036854775807; -pub const UINT_FAST8_MAX: u32 = 255; -pub const UINT_FAST16_MAX: i32 = -1; -pub const UINT_FAST32_MAX: i32 = -1; -pub const INTPTR_MIN: i64 = -9223372036854775808; -pub const INTPTR_MAX: u64 = 9223372036854775807; -pub const UINTPTR_MAX: i32 = -1; -pub const PTRDIFF_MIN: i64 = -9223372036854775808; -pub const PTRDIFF_MAX: u64 = 9223372036854775807; -pub const SIG_ATOMIC_MIN: i32 = -2147483648; -pub const SIG_ATOMIC_MAX: u32 = 2147483647; -pub const SIZE_MAX: i32 = -1; -pub const WINT_MIN: u32 = 0; -pub const WINT_MAX: u32 = 4294967295; -pub const COIN_BIG_DOUBLE: u32 = 0; -pub const COIN_LONG_WORK: u32 = 0; -pub type __u_char = ::std::os::raw::c_uchar; -pub type __u_short = ::std::os::raw::c_ushort; -pub type __u_int = ::std::os::raw::c_uint; -pub type __u_long = ::std::os::raw::c_ulong; -pub type __int8_t = ::std::os::raw::c_schar; -pub type __uint8_t = ::std::os::raw::c_uchar; -pub type __int16_t = ::std::os::raw::c_short; -pub type __uint16_t = ::std::os::raw::c_ushort; -pub type __int32_t = ::std::os::raw::c_int; -pub type __uint32_t = ::std::os::raw::c_uint; -pub type __int64_t = ::std::os::raw::c_long; -pub type __uint64_t = ::std::os::raw::c_ulong; -pub type __int_least8_t = __int8_t; -pub type __uint_least8_t = __uint8_t; -pub type __int_least16_t = __int16_t; -pub type __uint_least16_t = __uint16_t; -pub type __int_least32_t = __int32_t; -pub type __uint_least32_t = __uint32_t; -pub type __int_least64_t = __int64_t; -pub type __uint_least64_t = __uint64_t; -pub type __quad_t = ::std::os::raw::c_long; -pub type __u_quad_t = ::std::os::raw::c_ulong; -pub type __intmax_t = ::std::os::raw::c_long; -pub type __uintmax_t = ::std::os::raw::c_ulong; -pub type __dev_t = ::std::os::raw::c_ulong; -pub type __uid_t = ::std::os::raw::c_uint; -pub type __gid_t = ::std::os::raw::c_uint; -pub type __ino_t = ::std::os::raw::c_ulong; -pub type __ino64_t = ::std::os::raw::c_ulong; -pub type __mode_t = ::std::os::raw::c_uint; -pub type __nlink_t = ::std::os::raw::c_ulong; -pub type __off_t = ::std::os::raw::c_long; -pub type __off64_t = ::std::os::raw::c_long; -pub type __pid_t = ::std::os::raw::c_int; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct __fsid_t { - pub __val: [::std::os::raw::c_int; 2usize], -} -#[test] -fn bindgen_test_layout___fsid_t() { - assert_eq!( - ::std::mem::size_of::<__fsid_t>(), - 8usize, - concat!("Size of: ", stringify!(__fsid_t)) - ); - assert_eq!( - ::std::mem::align_of::<__fsid_t>(), - 4usize, - concat!("Alignment of ", stringify!(__fsid_t)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__fsid_t>())).__val as *const _ as usize }, - 0usize, - concat!("Offset of field: ", stringify!(__fsid_t), "::", stringify!(__val)) - ); -} -pub type __clock_t = ::std::os::raw::c_long; -pub type __rlim_t = ::std::os::raw::c_ulong; -pub type __rlim64_t = ::std::os::raw::c_ulong; -pub type __id_t = ::std::os::raw::c_uint; -pub type __time_t = ::std::os::raw::c_long; -pub type __useconds_t = ::std::os::raw::c_uint; -pub type __suseconds_t = ::std::os::raw::c_long; -pub type __suseconds64_t = ::std::os::raw::c_long; -pub type __daddr_t = ::std::os::raw::c_int; -pub type __key_t = ::std::os::raw::c_int; -pub type __clockid_t = ::std::os::raw::c_int; -pub type __timer_t = *mut ::std::os::raw::c_void; -pub type __blksize_t = ::std::os::raw::c_long; -pub type __blkcnt_t = ::std::os::raw::c_long; -pub type __blkcnt64_t = ::std::os::raw::c_long; -pub type __fsblkcnt_t = ::std::os::raw::c_ulong; -pub type __fsblkcnt64_t = ::std::os::raw::c_ulong; -pub type __fsfilcnt_t = ::std::os::raw::c_ulong; -pub type __fsfilcnt64_t = ::std::os::raw::c_ulong; -pub type __fsword_t = ::std::os::raw::c_long; -pub type __ssize_t = ::std::os::raw::c_long; -pub type __syscall_slong_t = ::std::os::raw::c_long; -pub type __syscall_ulong_t = ::std::os::raw::c_ulong; -pub type __loff_t = __off64_t; -pub type __caddr_t = *mut ::std::os::raw::c_char; -pub type __intptr_t = ::std::os::raw::c_long; -pub type __socklen_t = ::std::os::raw::c_uint; -pub type __sig_atomic_t = ::std::os::raw::c_int; -pub type int_least8_t = __int_least8_t; -pub type int_least16_t = __int_least16_t; -pub type int_least32_t = __int_least32_t; -pub type int_least64_t = __int_least64_t; -pub type uint_least8_t = __uint_least8_t; -pub type uint_least16_t = __uint_least16_t; -pub type uint_least32_t = __uint_least32_t; -pub type uint_least64_t = __uint_least64_t; -pub type int_fast8_t = ::std::os::raw::c_schar; -pub type int_fast16_t = ::std::os::raw::c_long; -pub type int_fast32_t = ::std::os::raw::c_long; -pub type int_fast64_t = ::std::os::raw::c_long; -pub type uint_fast8_t = ::std::os::raw::c_uchar; -pub type uint_fast16_t = ::std::os::raw::c_ulong; -pub type uint_fast32_t = ::std::os::raw::c_ulong; -pub type uint_fast64_t = ::std::os::raw::c_ulong; -pub type intmax_t = __intmax_t; -pub type uintmax_t = __uintmax_t; -pub type CoinBigIndex = ::std::os::raw::c_int; -pub type CoinByteArray = isize; -pub type CoinWorkDouble = f64; -#[doc = " For factorizations inheriting from CoinDenseFactorization -"] -#[doc = "leave partial conversions but back to double."] -pub type CoinFactorizationDouble2 = f64; -pub type CoinFactorizationDouble = f64; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct Clp_Simplex_s { - _unused: [u8; 0], -} -pub type Clp_Simplex = Clp_Simplex_s; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct Clp_Solve_s { - _unused: [u8; 0], -} -pub type Clp_Solve = Clp_Solve_s; -#[doc = " typedef for user call back."] -#[doc = ""] -#[doc = " The cvec are constructed so don't need to be const"] +pub const __bool_true_false_are_defined: u32 = 1; +pub const true_: u32 = 1; +pub const false_: u32 = 0; +pub type Clp_Simplex = ::std::os::raw::c_void; pub type clp_callback = ::std::option::Option< unsafe extern "C" fn( model: *mut Clp_Simplex, @@ -238,11 +11,48 @@ pub type clp_callback = ::std::option::Option< ndouble: ::std::os::raw::c_int, dvec: *const f64, nint: ::std::os::raw::c_int, - ivec: *const CoinBigIndex, + ivec: *const ::std::os::raw::c_int, + nchar: ::std::os::raw::c_int, + cvec: *mut *mut ::std::os::raw::c_char, + ), +>; +pub type Sbb_Model = ::std::os::raw::c_void; +pub type Cbc_Model = ::std::os::raw::c_void; +#[doc = " typedef for user call back.\nThe cvec are constructed so don't need to be const"] +pub type sbb_callback = ::std::option::Option< + unsafe extern "C" fn( + model: *mut Sbb_Model, + msgno: ::std::os::raw::c_int, + ndouble: ::std::os::raw::c_int, + dvec: *const f64, + nint: ::std::os::raw::c_int, + ivec: *const ::std::os::raw::c_int, nchar: ::std::os::raw::c_int, cvec: *mut *mut ::std::os::raw::c_char, ), >; +pub type cbc_callback = ::std::option::Option< + unsafe extern "C" fn( + model: *mut Cbc_Model, + msgno: ::std::os::raw::c_int, + ndouble: ::std::os::raw::c_int, + dvec: *const f64, + nint: ::std::os::raw::c_int, + ivec: *const ::std::os::raw::c_int, + nchar: ::std::os::raw::c_int, + cvec: *mut *mut ::std::os::raw::c_char, + ), +>; +#[doc = " typedef for cbc cut callback osiSolver needs to be an OsiSolverInterface object,\n osiCuts is an OsiCuts object and appdata is a pointer that will be passed to the cut\n generation, you can use it to point to a data structure with information about the original problem,\n for instance"] +pub type cbc_cut_callback = ::std::option::Option< + unsafe extern "C" fn( + osiSolver: *mut ::std::os::raw::c_void, + osiCuts: *mut ::std::os::raw::c_void, + appdata: *mut ::std::os::raw::c_void, + ), +>; +pub type CoinBigIndex = ::std::os::raw::c_int; +pub type Clp_Solve = ::std::os::raw::c_void; extern "C" { #[doc = " Clp library version number as string."] pub fn Clp_Version() -> *const ::std::os::raw::c_char; @@ -276,17 +86,7 @@ extern "C" { pub fn ClpSolve_delete(solve: *mut Clp_Solve); } extern "C" { - #[doc = " Loads a problem (the constraints on the"] - #[doc = "rows are given by lower and upper bounds). If a pointer is NULL then the"] - #[doc = "following values are the default:"] - #[doc = ""] - #[doc = "given in a standard column major ordered format (without gaps)."] + #[doc = " Loads a problem (the constraints on the\nrows are given by lower and upper bounds). If a pointer is NULL then the\nfollowing values are the default:\n\n/\n/** Just like the other loadProblem() method except that the matrix is\ngiven in a standard column major ordered format (without gaps)."] pub fn Clp_loadProblem( model: *mut Clp_Simplex, numcols: ::std::os::raw::c_int, @@ -320,9 +120,7 @@ extern "C" { ) -> ::std::os::raw::c_int; } extern "C" { - #[doc = " Write an mps file to the given filename */"] - #[doc = "Number across is 1 or 2."] - #[doc = "Use objSense = -1D to flip the objective function around."] + #[doc = " Write an mps file to the given filename */\n/** Format type is 0 = normal, 1 = extra or 2 = hex.\nNumber across is 1 or 2.\nUse objSense = -1D to flip the objective function around."] pub fn Clp_writeMps( model: *mut Clp_Simplex, filename: *const ::std::os::raw::c_char, @@ -405,13 +203,12 @@ extern "C" { pub fn Clp_chgObjCoefficients(model: *mut Clp_Simplex, objIn: *const f64); } extern "C" { - #[doc = " Change matrix coefficients"] pub fn Clp_modifyCoefficient( model: *mut Clp_Simplex, row: ::std::os::raw::c_int, column: ::std::os::raw::c_int, newElement: f64, - keepZero: ::std::os::raw::c_int, + keepZero: bool, ); } extern "C" { @@ -426,10 +223,6 @@ extern "C" { columnNames: *const *const ::std::os::raw::c_char, ); } -extern "C" { - #[doc = " The underlying ClpSimplex model"] - pub fn Clp_model(model: *mut Clp_Simplex) -> *mut ::std::os::raw::c_void; -} extern "C" { #[doc = " Number of rows"] pub fn Clp_numberRows(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; @@ -490,7 +283,7 @@ extern "C" { } extern "C" { #[doc = " Maximum number of iterations"] - pub fn Clp_maximumIterations(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; + pub fn maximumIterations(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; } extern "C" { pub fn Clp_setMaximumIterations(model: *mut Clp_Simplex, value: ::std::os::raw::c_int); @@ -507,12 +300,7 @@ extern "C" { pub fn Clp_hitMaximumIterations(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; } extern "C" { - #[doc = " Status of problem:"] - #[doc = "0 - optimal"] - #[doc = "1 - primal infeasible"] - #[doc = "2 - dual infeasible"] - #[doc = "3 - stopped on iterations etc"] - #[doc = "4 - stopped due to errors"] + #[doc = " Status of problem:\n0 - optimal\n1 - primal infeasible\n2 - dual infeasible\n3 - stopped on iterations etc\n4 - stopped due to errors"] pub fn Clp_status(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; } extern "C" { @@ -520,12 +308,7 @@ extern "C" { pub fn Clp_setProblemStatus(model: *mut Clp_Simplex, problemStatus: ::std::os::raw::c_int); } extern "C" { - #[doc = " Secondary status of problem - may get extended"] - #[doc = "0 - none"] - #[doc = "1 - primal infeasible because dual limit reached"] - #[doc = "2 - scaled problem optimal - unscaled has primal infeasibilities"] - #[doc = "3 - scaled problem optimal - unscaled has dual infeasibilities"] - #[doc = "4 - scaled problem optimal - unscaled has both dual and primal infeasibilities"] + #[doc = " Secondary status of problem - may get extended\n0 - none\n1 - primal infeasible because dual limit reached\n2 - scaled problem optimal - unscaled has primal infeasibilities\n3 - scaled problem optimal - unscaled has dual infeasibilities\n4 - scaled problem optimal - unscaled has both dual and primal infeasibilities"] pub fn Clp_secondaryStatus(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; } extern "C" { @@ -599,19 +382,11 @@ extern "C" { pub fn Clp_integerInformation(model: *mut Clp_Simplex) -> *mut ::std::os::raw::c_char; } extern "C" { - #[doc = " Gives Infeasibility ray."] - #[doc = ""] - #[doc = " Use Clp_freeRay to free the returned array."] - #[doc = ""] - #[doc = " @return infeasibility ray, or NULL returned if none/wrong."] + #[doc = " Gives Infeasibility ray.\n\n Use Clp_freeRay to free the returned array.\n\n @return infeasibility ray, or NULL returned if none/wrong."] pub fn Clp_infeasibilityRay(model: *mut Clp_Simplex) -> *mut f64; } extern "C" { - #[doc = " Gives ray in which the problem is unbounded."] - #[doc = ""] - #[doc = " Use Clp_freeRay to free the returned array."] - #[doc = ""] - #[doc = " @return unbounded ray, or NULL returned if none/wrong."] + #[doc = " Gives ray in which the problem is unbounded.\n\n Use Clp_freeRay to free the returned array.\n\n @return unbounded ray, or NULL returned if none/wrong."] pub fn Clp_unboundedRay(model: *mut Clp_Simplex) -> *mut f64; } extern "C" { @@ -650,8 +425,7 @@ extern "C" { pub fn Clp_getUserPointer(model: *mut Clp_Simplex) -> *mut ::std::os::raw::c_void; } extern "C" { - #[doc = " Pass in Callback function."] - #[doc = "Message numbers up to 1000000 are Clp, Coin ones have 1000000 added"] + #[doc = " Pass in Callback function.\nMessage numbers up to 1000000 are Clp, Coin ones have 1000000 added"] pub fn Clp_registerCallBack(model: *mut Clp_Simplex, userCallBack: clp_callback); } extern "C" { @@ -659,13 +433,7 @@ extern "C" { pub fn Clp_clearCallBack(model: *mut Clp_Simplex); } extern "C" { - #[doc = " Amount of print out:"] - #[doc = "0 - none"] - #[doc = "1 - just final"] - #[doc = "2 - just factorizations"] - #[doc = "3 - as 2 plus a bit more"] - #[doc = "4 - verbose"] - #[doc = "above that 8,16,32 etc just for selective debug"] + #[doc = " Amount of print out:\n0 - none\n1 - just final\n2 - just factorizations\n3 - as 2 plus a bit more\n4 - verbose\nabove that 8,16,32 etc just for selective debug"] pub fn Clp_setLogLevel(model: *mut Clp_Simplex, value: ::std::os::raw::c_int); } extern "C" { @@ -696,8 +464,7 @@ extern "C" { ); } extern "C" { - #[doc = " General solve algorithm which can do presolve."] - #[doc = "See ClpSolve.hpp for options"] + #[doc = " General solve algorithm which can do presolve.\nSee ClpSolve.hpp for options"] pub fn Clp_initialSolve(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; } extern "C" { @@ -741,19 +508,7 @@ extern "C" { pub fn Clp_scalingFlag(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; } extern "C" { - #[doc = " Crash - at present just aimed at dual, returns"] - #[doc = "-2 if dual preferred and crash basis created"] - #[doc = "-1 if dual preferred and all slack basis preferred"] - #[doc = "0 if basis going in was not all slack"] - #[doc = "1 if primal preferred and all slack basis preferred"] - #[doc = "2 if primal preferred and crash basis created."] - #[doc = ""] - #[doc = "if gap between bounds <=\"gap\" variables can be flipped"] - #[doc = ""] - #[doc = "If \"pivot\" is"] - #[doc = "0 No pivoting (so will just be choice of algorithm)"] - #[doc = "1 Simple pivoting e.g. gub"] - #[doc = "2 Mini iterations"] + #[doc = " Crash - at present just aimed at dual, returns\n-2 if dual preferred and crash basis created\n-1 if dual preferred and all slack basis preferred\n0 if basis going in was not all slack\n1 if primal preferred and all slack basis preferred\n2 if primal preferred and crash basis created.\n\nif gap between bounds <=\"gap\" variables can be flipped\n\nIf \"pivot\" is\n0 No pivoting (so will just be choice of algorithm)\n1 Simple pivoting e.g. gub\n2 Mini iterations"] pub fn Clp_crash(model: *mut Clp_Simplex, gap: f64, pivot: ::std::os::raw::c_int) -> ::std::os::raw::c_int; } extern "C" { @@ -779,13 +534,7 @@ extern "C" { pub fn Clp_setInfeasibilityCost(model: *mut Clp_Simplex, value: f64); } extern "C" { - #[doc = " Perturbation:"] - #[doc = "50 - switch on perturbation"] - #[doc = "100 - auto perturb if takes too long (1.0e-6 largest nonzero)"] - #[doc = "101 - we are perturbed"] - #[doc = "102 - don't try perturbing again"] - #[doc = "default is 100"] - #[doc = "others are for playing"] + #[doc = " Perturbation:\n50 - switch on perturbation\n100 - auto perturb if takes too long (1.0e-6 largest nonzero)\n101 - we are perturbed\n102 - don't try perturbing again\ndefault is 100\nothers are for playing"] pub fn Clp_perturbation(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; } extern "C" { @@ -816,21 +565,15 @@ extern "C" { pub fn Clp_numberPrimalInfeasibilities(model: *mut Clp_Simplex) -> ::std::os::raw::c_int; } extern "C" { - #[doc = " Save model to file, returns 0 if success. This is designed for"] - #[doc = "use outside algorithms so does not save iterating arrays etc."] - #[doc = "It does not save any messaging information."] - #[doc = "Does not save scaling values."] - #[doc = "It does not know about all types of virtual functions."] + #[doc = " Save model to file, returns 0 if success. This is designed for\nuse outside algorithms so does not save iterating arrays etc.\nIt does not save any messaging information.\nDoes not save scaling values.\nIt does not know about all types of virtual functions."] pub fn Clp_saveModel(model: *mut Clp_Simplex, fileName: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } extern "C" { - #[doc = " Restore model from file, returns 0 if success,"] - #[doc = "deletes current model"] + #[doc = " Restore model from file, returns 0 if success,\ndeletes current model"] pub fn Clp_restoreModel(model: *mut Clp_Simplex, fileName: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int; } extern "C" { - #[doc = " Just check solution (for external use) - sets sum of"] - #[doc = "infeasibilities etc"] + #[doc = " Just check solution (for external use) - sets sum of\ninfeasibilities etc"] pub fn Clp_checkSolution(model: *mut Clp_Simplex); } extern "C" { @@ -924,10 +667,6 @@ extern "C" { #[doc = " Objective value"] pub fn Clp_getObjValue(model: *mut Clp_Simplex) -> f64; } -extern "C" { - #[doc = " Set random seed"] - pub fn Clp_setRandomSeed(model: *mut Clp_Simplex, seed: ::std::os::raw::c_int); -} extern "C" { #[doc = " Print model for debugging purposes"] pub fn Clp_printModel(model: *mut Clp_Simplex, prefix: *const ::std::os::raw::c_char); @@ -950,27 +689,14 @@ extern "C" { pub fn ClpSolve_getSpecialOption(arg1: *mut Clp_Solve, which: ::std::os::raw::c_int) -> ::std::os::raw::c_int; } extern "C" { - #[doc = " method: (see ClpSolve::SolveType)"] - #[doc = "0 - dual simplex"] - #[doc = "1 - primal simplex"] - #[doc = "2 - primal or sprint"] - #[doc = "3 - barrier"] - #[doc = "4 - barrier no crossover"] - #[doc = "5 - automatic"] - #[doc = "6 - not implemented"] - #[doc = "-- pass extraInfo == -1 for default behavior"] + #[doc = " method: (see ClpSolve::SolveType)\n0 - dual simplex\n1 - primal simplex\n2 - primal or sprint\n3 - barrier\n4 - barrier no crossover\n5 - automatic\n6 - not implemented\n-- pass extraInfo == -1 for default behavior"] pub fn ClpSolve_setSolveType(arg1: *mut Clp_Solve, method: ::std::os::raw::c_int, extraInfo: ::std::os::raw::c_int); } extern "C" { pub fn ClpSolve_getSolveType(arg1: *mut Clp_Solve) -> ::std::os::raw::c_int; } extern "C" { - #[doc = " amount: (see ClpSolve::PresolveType)"] - #[doc = "0 - presolve on"] - #[doc = "1 - presolve off"] - #[doc = "2 - presolve number"] - #[doc = "3 - presolve number cost"] - #[doc = "-- pass extraInfo == -1 for default behavior"] + #[doc = " amount: (see ClpSolve::PresolveType)\n0 - presolve on\n1 - presolve off\n2 - presolve number\n3 - presolve number cost\n-- pass extraInfo == -1 for default behavior"] pub fn ClpSolve_setPresolveType( arg1: *mut Clp_Solve, amount: ::std::os::raw::c_int, diff --git a/clp-sys/vendor/Clp b/clp-sys/vendor/Clp index 9683fedd..7b9daa62 160000 --- a/clp-sys/vendor/Clp +++ b/clp-sys/vendor/Clp @@ -1 +1 @@ -Subproject commit 9683fedda4913cb13bfe323c64a87b970530869d +Subproject commit 7b9daa62d4c2710a368a17385913ce59d8c67b68 diff --git a/clp-sys/vendor/CoinUtils b/clp-sys/vendor/CoinUtils index 583f1210..5b98c4bb 160000 --- a/clp-sys/vendor/CoinUtils +++ b/clp-sys/vendor/CoinUtils @@ -1 +1 @@ -Subproject commit 583f1210b901e030725a88ac508c73f6c5b5fb10 +Subproject commit 5b98c4bb87ceebedb86c6e921dc4ad0af6166c8e diff --git a/pywr-book/book.toml b/pywr-book/book.toml index 3613edc5..241792d1 100644 --- a/pywr-book/book.toml +++ b/pywr-book/book.toml @@ -3,4 +3,4 @@ authors = ["James Tomlinson"] language = "en" multilingual = false src = "src" -title = "The Pywr Book" +title = "Pywr User Guide" diff --git a/pywr-book/src/SUMMARY.md b/pywr-book/src/SUMMARY.md index f9bbbccb..d4fbe8be 100644 --- a/pywr-book/src/SUMMARY.md +++ b/pywr-book/src/SUMMARY.md @@ -2,8 +2,6 @@ - [Introduction](./introduction.md) - [Getting started](./getting_started.md) - - [Installation](./installation.md) - - [Licensing](./licensing.md) - [Related projects](./related_projects.md) - [Core concepts](./concepts/README.md) - [The network](./concepts/the-network.md) diff --git a/pywr-book/src/getting_started.md b/pywr-book/src/getting_started.md index 69a5d543..f3719830 100644 --- a/pywr-book/src/getting_started.md +++ b/pywr-book/src/getting_started.md @@ -1 +1,36 @@ -# Getting started +# Installation + +Pywr is both a Rust library and a Python package. + +## Rust + +TBC + +## Python + +Pywr requires Python 3.9 or later. +It is currently not available on PyPI, but wheels are available from the GitHub [actions](https://github.com/pywr/pywr-next/actions) page. +Navigate to the latest successful build, and download the archive and extract the wheel for your platform. + +```bash +pip install pywr-2.0.0b0-cp312-none-win_amd64.whl +``` +> **Note**: That current Pywr v2.x is in pre-release and may not be suitable for production use. +> If you require Pywr v1.x please use `pip install pywr<2`. + + +# Running a model + +Pywr is a modelling system for simulating water resources systems. +Models are defined using a JSON schema, and can be run using the `pywr` command line tool. +Below is an example of a simple model definition `simple1.json`: + +```json +{{#include ../../pywr-schema/src/test_models/simple1.json}} +``` + +To run the model, use the `pywr` command line tool: + +```bash +python -m pywr run simple1.json +``` diff --git a/pywr-book/src/installation.md b/pywr-book/src/installation.md deleted file mode 100644 index 25267fe2..00000000 --- a/pywr-book/src/installation.md +++ /dev/null @@ -1 +0,0 @@ -# Installation diff --git a/pywr-book/src/licensing.md b/pywr-book/src/licensing.md deleted file mode 100644 index a489b0ce..00000000 --- a/pywr-book/src/licensing.md +++ /dev/null @@ -1 +0,0 @@ -# Licensing diff --git a/pywr-cli/Cargo.toml b/pywr-cli/Cargo.toml index 48fb7812..480d8f5f 100644 --- a/pywr-cli/Cargo.toml +++ b/pywr-cli/Cargo.toml @@ -15,10 +15,10 @@ categories = ["science", "simulation"] [dependencies] clap = { version="4.0", features=["derive"] } anyhow = "1.0.69" - +tracing = { workspace = true } +tracing-subscriber = { version ="0.3.17", features=["env-filter"] } rand = "0.8.5" rand_chacha = "0.3.1" -time = { workspace = true, features = ["serde", "serde-well-known", "serde-human-readable", "macros"] } serde = { workspace = true } serde_json = { workspace = true } pywr-v1-schema = { workspace = true } diff --git a/pywr-cli/src/main.rs b/pywr-cli/src/main.rs index 7ca3f5fe..f49c3124 100644 --- a/pywr-cli/src/main.rs +++ b/pywr-cli/src/main.rs @@ -1,3 +1,6 @@ +mod tracing; + +use crate::tracing::setup_tracing; use anyhow::{Context, Result}; use clap::{Parser, Subcommand, ValueEnum}; #[cfg(feature = "ipm-ocl")] @@ -8,7 +11,6 @@ use pywr_core::solvers::{HighsSolver, HighsSolverSettings}; #[cfg(feature = "ipm-simd")] use pywr_core::solvers::{SimdIpmF64Solver, SimdIpmSolverSettings}; use pywr_core::test_utils::make_random_model; -use pywr_core::tracing::setup_tracing; use pywr_schema::model::{PywrModel, PywrMultiNetworkModel}; use pywr_schema::ConversionError; use rand::SeedableRng; diff --git a/pywr-core/src/tracing.rs b/pywr-cli/src/tracing.rs similarity index 100% rename from pywr-core/src/tracing.rs rename to pywr-cli/src/tracing.rs diff --git a/pywr-core/Cargo.toml b/pywr-core/Cargo.toml index 74e59fbf..169adc18 100644 --- a/pywr-core/Cargo.toml +++ b/pywr-core/Cargo.toml @@ -15,30 +15,29 @@ categories = ["science", "simulation"] [dependencies] libc = "0.2.97" -thiserror = { workspace = true } -ndarray = { workspace = true } -num = { workspace = true } +thiserror = { workspace = true } +ndarray = { workspace = true } +num = { workspace = true } float-cmp = "0.9.0" -hdf5 = { workspace = true } -hdf5-sys = { workspace = true } -csv = { workspace = true } -clp-sys = { path = "../clp-sys" } +hdf5 = { workspace = true } +csv = { workspace = true } +clp-sys = { path = "../clp-sys", version = "0.1.0" } ipm-ocl = { path = "../ipm-ocl", optional = true } ipm-simd = { path = "../ipm-simd", optional = true } -time = { workspace = true, features = ["macros"] } tracing = { workspace = true } -tracing-subscriber = { version ="0.3.17", features=["env-filter"] } highs-sys = { git = "https://github.com/jetuk/highs-sys", branch="fix-build-libz-linking", optional = true } # highs-sys = { path = "../../highs-sys" } nalgebra = "0.32.3" +chrono = { workspace = true } +polars = { workspace = true } -pyo3 = { workspace = true } +pyo3 = { workspace = true, features = ["chrono"] } rayon = "1.6.1" -rhai = { version="1.12.0", features=["sync"] } +rhai = { version = "1.12.0", features = ["sync"] } # OpenCL ocl = { version = "0.19", optional = true } diff --git a/pywr-core/src/aggregated_node.rs b/pywr-core/src/aggregated_node.rs index b2a2166d..baa70337 100644 --- a/pywr-core/src/aggregated_node.rs +++ b/pywr-core/src/aggregated_node.rs @@ -72,6 +72,36 @@ pub struct AggregatedNode { factors: Option, } +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct NodeFactor { + pub index: NodeIndex, + pub factor: f64, +} + +impl NodeFactor { + fn new(node: NodeIndex, factor: f64) -> Self { + Self { index: node, factor } + } +} + +/// A pair of nodes and their factors +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct NodeFactorPair { + pub node0: NodeFactor, + pub node1: NodeFactor, +} + +impl NodeFactorPair { + fn new(node0: NodeFactor, node1: NodeFactor) -> Self { + Self { node0, node1 } + } + + /// Return the ratio of the two factors (node0 / node1) + pub fn ratio(&self) -> f64 { + self.node0.factor / self.node1.factor + } +} + impl AggregatedNode { pub fn new( index: &AggregatedNodeIndex, @@ -130,11 +160,7 @@ impl AggregatedNode { /// Return normalised factor pairs /// - pub fn get_norm_factor_pairs( - &self, - model: &Network, - state: &State, - ) -> Option> { + pub fn get_norm_factor_pairs(&self, model: &Network, state: &State) -> Option> { if let Some(factors) = &self.factors { let pairs = match factors { Factors::Proportion(prop_factors) => { @@ -205,7 +231,7 @@ fn get_norm_proportional_factor_pairs( nodes: &[NodeIndex], model: &Network, state: &State, -) -> Vec<((NodeIndex, f64), (NodeIndex, f64))> { +) -> Vec { if factors.len() != nodes.len() - 1 { panic!("Found {} proportional factors and {} nodes in aggregated node. The number of proportional factors should equal one less than the number of nodes.", factors.len(), nodes.len()); } @@ -232,8 +258,8 @@ fn get_norm_proportional_factor_pairs( nodes .iter() .skip(1) - .zip(values.into_iter()) - .map(move |(&n1, f1)| ((n0, f0), (n1, f1))) + .zip(values) + .map(move |(&n1, f1)| NodeFactorPair::new(NodeFactor::new(n0, f0), NodeFactor::new(n1, f1))) .collect::>() } @@ -243,7 +269,7 @@ fn get_norm_ratio_factor_pairs( nodes: &[NodeIndex], model: &Network, state: &State, -) -> Vec<((NodeIndex, f64), (NodeIndex, f64))> { +) -> Vec { if factors.len() != nodes.len() { panic!("Found {} ratio factors and {} nodes in aggregated node. The number of ratio factors should equal the number of nodes.", factors.len(), nodes.len()); } @@ -255,7 +281,12 @@ fn get_norm_ratio_factor_pairs( .iter() .zip(factors) .skip(1) - .map(move |(&n1, f1)| ((n0, f0), (n1, f1.get_value(model, state).unwrap()))) + .map(move |(&n1, f1)| { + NodeFactorPair::new( + NodeFactor::new(n0, f0), + NodeFactor::new(n1, f1.get_value(model, state).unwrap()), + ) + }) .collect::>() } diff --git a/pywr-core/src/derived_metric.rs b/pywr-core/src/derived_metric.rs index eff4b9af..767d609f 100644 --- a/pywr-core/src/derived_metric.rs +++ b/pywr-core/src/derived_metric.rs @@ -49,7 +49,7 @@ impl DerivedMetric { pub fn before(&self, timestep: &Timestep, network: &Network, state: &State) -> Result, PywrError> { // On the first time-step set the initial value if timestep.is_first() { - self.compute(network, state).map(|v| Some(v)) + self.compute(network, state).map(Some) } else { Ok(None) } diff --git a/pywr-core/src/lib.rs b/pywr-core/src/lib.rs index 34d123d9..d51f398f 100644 --- a/pywr-core/src/lib.rs +++ b/pywr-core/src/lib.rs @@ -27,7 +27,6 @@ pub mod solvers; pub mod state; pub mod test_utils; pub mod timestep; -pub mod tracing; pub mod virtual_storage; #[derive(Error, Debug, PartialEq, Eq)] @@ -156,6 +155,12 @@ pub enum PywrError { NetworkIndexNotFound(usize), #[error("parameters do not provide an initial value")] ParameterNoInitialValue, + #[error("parameter state not found for parameter index {0}")] + ParameterStateNotFound(ParameterIndex), + #[error("Could not create timestep range due to following error: {0}")] + TimestepRangeGenerationError(String), + #[error("Could not create timesteps for frequency '{0}'")] + TimestepGenerationError(String), } // Python errors diff --git a/pywr-core/src/models/mod.rs b/pywr-core/src/models/mod.rs index e32db4c0..a2140206 100644 --- a/pywr-core/src/models/mod.rs +++ b/pywr-core/src/models/mod.rs @@ -3,8 +3,9 @@ mod simple; use crate::scenario::{ScenarioDomain, ScenarioGroupCollection}; use crate::timestep::{TimeDomain, Timestepper}; +use crate::PywrError; pub use multi::{MultiNetworkModel, MultiNetworkTransferIndex}; -pub use simple::Model; +pub use simple::{Model, ModelState}; #[derive(Debug)] pub struct ModelDomain { @@ -17,11 +18,11 @@ impl ModelDomain { Self { time, scenarios } } - pub fn from(timestepper: Timestepper, scenario_collection: ScenarioGroupCollection) -> Self { - Self { - time: timestepper.into(), + pub fn from(timestepper: Timestepper, scenario_collection: ScenarioGroupCollection) -> Result { + Ok(Self { + time: TimeDomain::try_from(timestepper)?, scenarios: scenario_collection.into(), - } + }) } pub fn time(&self) -> &TimeDomain { @@ -37,12 +38,15 @@ impl ModelDomain { } } -impl From for ModelDomain { - fn from(value: Timestepper) -> Self { - Self { - time: value.into(), +impl TryFrom for ModelDomain { + type Error = PywrError; + + fn try_from(value: Timestepper) -> Result { + let time = TimeDomain::try_from(value)?; + Ok(Self { + time, scenarios: ScenarioGroupCollection::default().into(), - } + }) } } diff --git a/pywr-core/src/models/multi.rs b/pywr-core/src/models/multi.rs index ee290cc3..f3531df3 100644 --- a/pywr-core/src/models/multi.rs +++ b/pywr-core/src/models/multi.rs @@ -6,6 +6,7 @@ use crate::solvers::{Solver, SolverSettings}; use crate::timestep::Timestep; use crate::PywrError; use std::any::Any; +use std::cmp::Ordering; use std::fmt; use std::fmt::{Display, Formatter}; use std::num::NonZeroUsize; @@ -22,12 +23,10 @@ enum OtherNetworkIndex { impl OtherNetworkIndex { fn new(from_idx: usize, to_idx: usize) -> Self { - if from_idx == to_idx { - panic!("Cannot create OtherNetworkIndex to self.") - } else if from_idx < to_idx { - Self::Before(NonZeroUsize::new(to_idx - from_idx).unwrap()) - } else { - Self::After(NonZeroUsize::new(from_idx - to_idx).unwrap()) + match from_idx.cmp(&to_idx) { + Ordering::Equal => panic!("Cannot create OtherNetworkIndex to self."), + Ordering::Less => Self::Before(NonZeroUsize::new(to_idx - from_idx).unwrap()), + Ordering::Greater => Self::After(NonZeroUsize::new(from_idx - to_idx).unwrap()), } } } @@ -158,9 +157,9 @@ impl MultiNetworkModel { for entry in &self.networks { let state = entry .network - .setup_network(×teps, &scenario_indices, entry.parameters.len())?; + .setup_network(timesteps, scenario_indices, entry.parameters.len())?; let recorder_state = entry.network.setup_recorders(&self.domain)?; - let solver = entry.network.setup_solver::(&scenario_indices, settings)?; + let solver = entry.network.setup_solver::(scenario_indices, settings)?; states.push(state); recorder_states.push(recorder_state); @@ -197,10 +196,10 @@ impl MultiNetworkModel { scenario_index, &this_model.parameters, this_models_state, - &before_models, - &before, - &after_models, - &after, + before_models, + before, + after_models, + after, )?; } @@ -368,7 +367,7 @@ mod tests { let mut scenario_collection = ScenarioGroupCollection::default(); scenario_collection.add_group("test-scenario", 2); - let mut multi_model = MultiNetworkModel::new(ModelDomain::from(timestepper, scenario_collection)); + let mut multi_model = MultiNetworkModel::new(ModelDomain::from(timestepper, scenario_collection).unwrap()); let test_scenario_group_idx = multi_model .domain() diff --git a/pywr-core/src/models/simple.rs b/pywr-core/src/models/simple.rs index 856dcec6..71c04c45 100644 --- a/pywr-core/src/models/simple.rs +++ b/pywr-core/src/models/simple.rs @@ -72,9 +72,9 @@ impl Model { let timesteps = self.domain.time.timesteps(); let scenario_indices = self.domain.scenarios.indices(); - let state = self.network.setup_network(×teps, &scenario_indices, 0)?; + let state = self.network.setup_network(timesteps, scenario_indices, 0)?; let recorder_state = self.network.setup_recorders(&self.domain)?; - let solvers = self.network.setup_solver::(&scenario_indices, settings)?; + let solvers = self.network.setup_solver::(scenario_indices, settings)?; Ok(ModelState { current_time_step_idx: 0, @@ -91,11 +91,11 @@ impl Model { let timesteps = self.domain.time.timesteps(); let scenario_indices = self.domain.scenarios.indices(); - let state = self.network.setup_network(×teps, &scenario_indices, 0)?; + let state = self.network.setup_network(timesteps, scenario_indices, 0)?; let recorder_state = self.network.setup_recorders(&self.domain)?; let solvers = self .network - .setup_multi_scenario_solver::(&scenario_indices, settings)?; + .setup_multi_scenario_solver::(scenario_indices, settings)?; Ok(ModelState { current_time_step_idx: 0, @@ -132,12 +132,12 @@ impl Model { // State is mutated in-place pool.install(|| { self.network - .step_par(timestep, &scenario_indices, solvers, network_state, timings) + .step_par(timestep, scenario_indices, solvers, network_state, timings) })?; } None => { self.network - .step(timestep, &scenario_indices, solvers, network_state, timings)?; + .step(timestep, scenario_indices, solvers, network_state, timings)?; } } @@ -178,7 +178,7 @@ impl Model { // State is mutated in-place thread_pool.install(|| { self.network - .step_multi_scenario(timestep, &scenario_indices, solvers, network_state, timings) + .step_multi_scenario(timestep, scenario_indices, solvers, network_state, timings) })?; let start_r_save = Instant::now(); diff --git a/pywr-core/src/network.rs b/pywr-core/src/network.rs index 5a500427..74cdead0 100644 --- a/pywr-core/src/network.rs +++ b/pywr-core/src/network.rs @@ -5,11 +5,11 @@ use crate::edge::{EdgeIndex, EdgeVec}; use crate::metric::Metric; use crate::models::ModelDomain; use crate::node::{ConstraintValue, Node, NodeVec, StorageInitialVolume}; -use crate::parameters::{MultiValueParameterIndex, ParameterType}; +use crate::parameters::{MultiValueParameterIndex, ParameterType, VariableConfig}; use crate::recorders::{MetricSet, MetricSetIndex, MetricSetState}; use crate::scenario::ScenarioIndex; use crate::solvers::{MultiStateSolver, Solver, SolverFeatures, SolverTimings}; -use crate::state::{ParameterStates, State}; +use crate::state::{ParameterStates, State, StateBuilder}; use crate::timestep::Timestep; use crate::virtual_storage::{VirtualStorage, VirtualStorageIndex, VirtualStorageReset, VirtualStorageVec}; use crate::{parameters, recorders, IndexParameterIndex, NodeIndex, ParameterIndex, PywrError, RecorderIndex}; @@ -18,6 +18,7 @@ use std::any::Any; use std::collections::HashSet; use std::num::NonZeroUsize; use std::ops::Deref; +use std::slice::{Iter, IterMut}; use std::time::Duration; use std::time::Instant; use tracing::info; @@ -171,6 +172,16 @@ impl NetworkState { pub fn parameter_states_mut(&mut self, scenario_index: &ScenarioIndex) -> &mut ParameterStates { &mut self.parameter_internal_states[scenario_index.index] } + + /// Returns an iterator of immutable parameter states for each scenario. + pub fn iter_parameter_states(&self) -> Iter<'_, ParameterStates> { + self.parameter_internal_states.iter() + } + + /// Returns an iterator that allows modifying the parameter states for each scenario. + pub fn iter_parameter_states_mut(&mut self) -> IterMut<'_, ParameterStates> { + self.parameter_internal_states.iter_mut() + } } /// A Pywr network containing nodes, edges, parameters, metric sets, etc. @@ -251,16 +262,16 @@ impl Network { .map(|p| p.setup(timesteps, scenario_index)) .collect::, _>>()?; - let state = State::new( - initial_node_states, - self.edges.len(), - initial_virtual_storage_states, - initial_values_states.len(), - initial_indices_states.len(), - initial_multi_param_states.len(), - self.derived_metrics.len(), - num_inter_network_transfers, - ); + let state_builder = StateBuilder::new(initial_node_states, self.edges.len()) + .with_virtual_storage_states(initial_virtual_storage_states) + .with_value_parameters(initial_values_states.len()) + .with_index_parameters(initial_indices_states.len()) + .with_multi_parameters(initial_multi_param_states.len()) + .with_derived_metrics(self.derived_metrics.len()) + .with_inter_network_transfers(num_inter_network_transfers); + + let state = state_builder.build(); + states.push(state); parameter_internal_states.push(ParameterStates::new( @@ -1333,7 +1344,7 @@ impl Network { /// Add a [`MetricSet`] to the network. pub fn add_metric_set(&mut self, metric_set: MetricSet) -> Result { - if let Ok(_) = self.get_metric_set_by_name(&metric_set.name()) { + if self.get_metric_set_by_name(metric_set.name()).is_ok() { return Err(PywrError::MetricSetNameAlreadyExists(metric_set.name().to_string())); } @@ -1346,7 +1357,7 @@ impl Network { pub fn get_metric_set(&self, index: MetricSetIndex) -> Result<&MetricSet, PywrError> { self.metric_sets .get(*index) - .ok_or_else(|| PywrError::MetricSetIndexNotFound(index)) + .ok_or(PywrError::MetricSetIndexNotFound(index)) } /// Get a ['MetricSet'] by its name. @@ -1405,44 +1416,188 @@ impl Network { Ok(edge_index) } - /// Set the variable values on the parameter a index `['idx']. - pub fn set_f64_parameter_variable_values(&mut self, idx: ParameterIndex, values: &[f64]) -> Result<(), PywrError> { - match self.parameters.get_mut(*idx.deref()) { - Some(parameter) => match parameter.as_f64_variable_mut() { - Some(variable) => variable.set_variables(values), + /// Set the variable values on the parameter [`parameter_index`]. + /// + /// This will update the internal state of the parameter with the new values for all scenarios. + pub fn set_f64_parameter_variable_values( + &self, + parameter_index: ParameterIndex, + values: &[f64], + variable_config: &dyn VariableConfig, + state: &mut NetworkState, + ) -> Result<(), PywrError> { + match self.parameters.get(*parameter_index.deref()) { + Some(parameter) => match parameter.as_f64_variable() { + Some(variable) => { + // Iterate over all scenarios and set the variable values + for parameter_states in state.iter_parameter_states_mut() { + let internal_state = parameter_states + .get_mut_value_state(parameter_index) + .ok_or(PywrError::ParameterStateNotFound(parameter_index))?; + + variable.set_variables(values, variable_config, internal_state)?; + } + + Ok(()) + } + None => Err(PywrError::ParameterTypeNotVariable), + }, + None => Err(PywrError::ParameterIndexNotFound(parameter_index)), + } + } + + /// Set the variable values on the parameter [`parameter_index`] and scenario [`scenario_index`]. + /// + /// Only the internal state of the parameter for the given scenario will be updated. + pub fn set_f64_parameter_variable_values_for_scenario( + &self, + parameter_index: ParameterIndex, + scenario_index: ScenarioIndex, + values: &[f64], + variable_config: &dyn VariableConfig, + state: &mut NetworkState, + ) -> Result<(), PywrError> { + match self.parameters.get(*parameter_index.deref()) { + Some(parameter) => match parameter.as_f64_variable() { + Some(variable) => { + let internal_state = state + .parameter_states_mut(&scenario_index) + .get_mut_value_state(parameter_index) + .ok_or(PywrError::ParameterStateNotFound(parameter_index))?; + variable.set_variables(values, variable_config, internal_state) + } None => Err(PywrError::ParameterTypeNotVariable), }, - None => Err(PywrError::ParameterIndexNotFound(idx)), + None => Err(PywrError::ParameterIndexNotFound(parameter_index)), } } /// Return a vector of the current values of active variable parameters. - pub fn get_f64_parameter_variable_values(&self) -> Vec { - self.parameters - .iter() - .filter_map(|p| p.as_f64_variable().filter(|v| v.is_active()).map(|v| v.get_variables())) - .flatten() - .collect() + pub fn get_f64_parameter_variable_values_for_scenario( + &self, + parameter_index: ParameterIndex, + scenario_index: ScenarioIndex, + state: &NetworkState, + ) -> Result>, PywrError> { + match self.parameters.get(*parameter_index.deref()) { + Some(parameter) => match parameter.as_f64_variable() { + Some(variable) => { + let internal_state = state + .parameter_states(&scenario_index) + .get_value_state(parameter_index) + .ok_or(PywrError::ParameterStateNotFound(parameter_index))?; + + Ok(variable.get_variables(internal_state)) + } + None => Err(PywrError::ParameterTypeNotVariable), + }, + None => Err(PywrError::ParameterIndexNotFound(parameter_index)), + } } - /// Set the variable values on the parameter a index `['idx']. - pub fn set_u32_parameter_variable_values(&mut self, idx: ParameterIndex, values: &[u32]) -> Result<(), PywrError> { - match self.parameters.get_mut(*idx.deref()) { - Some(parameter) => match parameter.as_u32_variable_mut() { - Some(variable) => variable.set_variables(values), + pub fn get_f64_parameter_variable_values( + &self, + parameter_index: ParameterIndex, + state: &NetworkState, + ) -> Result>>, PywrError> { + match self.parameters.get(*parameter_index.deref()) { + Some(parameter) => match parameter.as_f64_variable() { + Some(variable) => { + let values = state + .iter_parameter_states() + .map(|parameter_states| { + let internal_state = parameter_states + .get_value_state(parameter_index) + .ok_or(PywrError::ParameterStateNotFound(parameter_index))?; + + Ok(variable.get_variables(internal_state)) + }) + .collect::>()?; + + Ok(values) + } None => Err(PywrError::ParameterTypeNotVariable), }, - None => Err(PywrError::ParameterIndexNotFound(idx)), + None => Err(PywrError::ParameterIndexNotFound(parameter_index)), + } + } + + /// Set the variable values on the parameter [`parameter_index`]. + /// + /// This will update the internal state of the parameter with the new values for scenarios. + pub fn set_u32_parameter_variable_values( + &self, + parameter_index: ParameterIndex, + values: &[u32], + variable_config: &dyn VariableConfig, + state: &mut NetworkState, + ) -> Result<(), PywrError> { + match self.parameters.get(*parameter_index.deref()) { + Some(parameter) => match parameter.as_u32_variable() { + Some(variable) => { + // Iterate over all scenarios and set the variable values + for parameter_states in state.iter_parameter_states_mut() { + let internal_state = parameter_states + .get_mut_value_state(parameter_index) + .ok_or(PywrError::ParameterStateNotFound(parameter_index))?; + + variable.set_variables(values, variable_config, internal_state)?; + } + + Ok(()) + } + None => Err(PywrError::ParameterTypeNotVariable), + }, + None => Err(PywrError::ParameterIndexNotFound(parameter_index)), + } + } + + /// Set the variable values on the parameter [`parameter_index`] and scenario [`scenario_index`]. + /// + /// Only the internal state of the parameter for the given scenario will be updated. + pub fn set_u32_parameter_variable_values_for_scenario( + &self, + parameter_index: ParameterIndex, + scenario_index: ScenarioIndex, + values: &[u32], + variable_config: &dyn VariableConfig, + state: &mut NetworkState, + ) -> Result<(), PywrError> { + match self.parameters.get(*parameter_index.deref()) { + Some(parameter) => match parameter.as_u32_variable() { + Some(variable) => { + let internal_state = state + .parameter_states_mut(&scenario_index) + .get_mut_value_state(parameter_index) + .ok_or(PywrError::ParameterIndexNotFound(parameter_index))?; + variable.set_variables(values, variable_config, internal_state) + } + None => Err(PywrError::ParameterTypeNotVariable), + }, + None => Err(PywrError::ParameterIndexNotFound(parameter_index)), } } /// Return a vector of the current values of active variable parameters. - pub fn get_u32_parameter_variable_values(&self) -> Vec { - self.parameters - .iter() - .filter_map(|p| p.as_u32_variable().filter(|v| v.is_active()).map(|v| v.get_variables())) - .flatten() - .collect() + pub fn get_u32_parameter_variable_values_for_scenario( + &self, + parameter_index: ParameterIndex, + scenario_index: ScenarioIndex, + state: &NetworkState, + ) -> Result>, PywrError> { + match self.parameters.get(*parameter_index.deref()) { + Some(parameter) => match parameter.as_u32_variable() { + Some(variable) => { + let internal_state = state + .parameter_states(&scenario_index) + .get_value_state(parameter_index) + .ok_or(PywrError::ParameterStateNotFound(parameter_index))?; + Ok(variable.get_variables(internal_state)) + } + None => Err(PywrError::ParameterTypeNotVariable), + }, + None => Err(PywrError::ParameterIndexNotFound(parameter_index)), + } } } @@ -1452,7 +1607,7 @@ mod tests { use crate::metric::Metric; use crate::network::Network; use crate::node::{Constraint, ConstraintValue}; - use crate::parameters::{ActivationFunction, ControlCurveInterpolatedParameter, Parameter, VariableParameter}; + use crate::parameters::{ActivationFunction, ControlCurveInterpolatedParameter, Parameter}; use crate::recorders::AssertionRecorder; use crate::scenario::{ScenarioDomain, ScenarioGroupCollection, ScenarioIndex}; #[cfg(feature = "clipm")] @@ -1538,7 +1693,7 @@ mod tests { let mut network = Network::default(); let _node_index = network.add_input_node("input", None).unwrap(); - let input_max_flow = parameters::ConstantParameter::new("my-constant", 10.0, None); + let input_max_flow = parameters::ConstantParameter::new("my-constant", 10.0); let parameter = network.add_parameter(Box::new(input_max_flow)).unwrap(); // assign the new parameter to one of the nodes. @@ -1777,35 +1932,44 @@ mod tests { #[test] /// Test the variable API fn test_variable_api() { - let mut network = Network::default(); - let _node_index = network.add_input_node("input", None).unwrap(); + let mut model = simple_model(1); let variable = ActivationFunction::Unit { min: 0.0, max: 10.0 }; - let input_max_flow = parameters::ConstantParameter::new("my-constant", 10.0, Some(variable)); + let input_max_flow = parameters::ConstantParameter::new("my-constant", 10.0); assert!(input_max_flow.can_be_f64_variable()); - assert!(input_max_flow.is_f64_variable_active()); - assert!(input_max_flow.is_active()); - let input_max_flow_idx = network.add_parameter(Box::new(input_max_flow)).unwrap(); + let input_max_flow_idx = model.network_mut().add_parameter(Box::new(input_max_flow)).unwrap(); // assign the new parameter to one of the nodes. - let node = network.get_mut_node_by_name("input", None).unwrap(); + let node = model.network_mut().get_mut_node_by_name("input", None).unwrap(); node.set_constraint( ConstraintValue::Metric(Metric::ParameterValue(input_max_flow_idx)), Constraint::MaxFlow, ) .unwrap(); - let variable_values = network.get_f64_parameter_variable_values(); - assert_eq!(variable_values, vec![10.0]); + let mut state = model.setup::(&ClpSolverSettings::default()).unwrap(); + + // Initially the variable value should be unset + let variable_values = model + .network_mut() + .get_f64_parameter_variable_values(input_max_flow_idx, state.network_state()) + .unwrap(); + assert_eq!(variable_values, vec![None]); // Update the variable values - network - .set_f64_parameter_variable_values(input_max_flow_idx, &[5.0]) + model + .network_mut() + .set_f64_parameter_variable_values(input_max_flow_idx, &[5.0], &variable, state.network_state_mut()) + .unwrap(); + + // After update the variable value should match what was set + let variable_values = model + .network_mut() + .get_f64_parameter_variable_values(input_max_flow_idx, state.network_state()) .unwrap(); - let variable_values = network.get_f64_parameter_variable_values(); - assert_eq!(variable_values, vec![5.0]); + assert_eq!(variable_values, vec![Some(vec![5.0])]); } } diff --git a/pywr-core/src/parameters/asymmetric.rs b/pywr-core/src/parameters/asymmetric.rs index c079d175..775fa535 100644 --- a/pywr-core/src/parameters/asymmetric.rs +++ b/pywr-core/src/parameters/asymmetric.rs @@ -1,5 +1,5 @@ use crate::network::Network; -use crate::parameters::{downcast_internal_state, IndexParameter, IndexValue, ParameterMeta}; +use crate::parameters::{downcast_internal_state_mut, IndexParameter, IndexValue, ParameterMeta}; use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; use crate::timestep::Timestep; @@ -47,7 +47,7 @@ impl IndexParameter for AsymmetricSwitchIndexParameter { }; // Downcast the internal state to the correct type - let current_state = downcast_internal_state::(internal_state); + let current_state = downcast_internal_state_mut::(internal_state); if *current_state > 0 { if on_value > 0 { diff --git a/pywr-core/src/parameters/constant.rs b/pywr-core/src/parameters/constant.rs index cd7e0ff1..658ee86f 100644 --- a/pywr-core/src/parameters/constant.rs +++ b/pywr-core/src/parameters/constant.rs @@ -1,5 +1,8 @@ use crate::network::Network; -use crate::parameters::{downcast_internal_state, ActivationFunction, Parameter, ParameterMeta, VariableParameter}; +use crate::parameters::{ + downcast_internal_state_mut, downcast_internal_state_ref, downcast_variable_config_ref, ActivationFunction, + Parameter, ParameterMeta, VariableConfig, VariableParameter, +}; use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; use crate::timestep::Timestep; @@ -9,15 +12,27 @@ use std::any::Any; pub struct ConstantParameter { meta: ParameterMeta, value: f64, - variable: Option, } +// We store this internal value as an Option so that it can be updated by the variable API +type InternalValue = Option; + impl ConstantParameter { - pub fn new(name: &str, value: f64, variable: Option) -> Self { + pub fn new(name: &str, value: f64) -> Self { Self { meta: ParameterMeta::new(name), value, - variable, + } + } + + /// Return the current value. + /// + /// If the internal state is None, the value is returned directly. Otherwise, the internal value must + /// have come from the variable API and is passed through the activation function. + fn value(&self, internal_state: &Option>) -> f64 { + match downcast_internal_state_ref::(internal_state) { + Some(value) => *value, + None => self.value, } } } @@ -36,7 +51,8 @@ impl Parameter for ConstantParameter { _timesteps: &[Timestep], _scenario_index: &ScenarioIndex, ) -> Result>, PywrError> { - Ok(Some(Box::new(self.value))) + let value: Option = None; + Ok(Some(Box::new(value))) } fn compute( @@ -47,9 +63,7 @@ impl Parameter for ConstantParameter { _state: &State, internal_state: &mut Option>, ) -> Result { - let value = downcast_internal_state::(internal_state); - - Ok(*value) + Ok(self.value(internal_state)) } fn as_f64_variable(&self) -> Option<&dyn VariableParameter> { @@ -62,39 +76,76 @@ impl Parameter for ConstantParameter { } impl VariableParameter for ConstantParameter { - fn is_active(&self) -> bool { - self.variable.is_some() + fn meta(&self) -> &ParameterMeta { + &self.meta } - fn size(&self) -> usize { + fn size(&self, _variable_config: &dyn VariableConfig) -> usize { 1 } - fn set_variables(&mut self, values: &[f64]) -> Result<(), PywrError> { + fn set_variables( + &self, + values: &[f64], + variable_config: &dyn VariableConfig, + internal_state: &mut Option>, + ) -> Result<(), PywrError> { + let activation_function = downcast_variable_config_ref::(variable_config); + if values.len() == 1 { - let variable = self.variable.ok_or(PywrError::ParameterVariableNotActive)?; - self.value = variable.apply(values[0]); + let value = downcast_internal_state_mut::(internal_state); + *value = Some(activation_function.apply(values[0])); Ok(()) } else { Err(PywrError::ParameterVariableValuesIncorrectLength) } } - fn get_variables(&self) -> Vec { - vec![self.value] + fn get_variables(&self, internal_state: &Option>) -> Option> { + downcast_internal_state_ref::(internal_state) + .as_ref() + .map(|value| vec![*value]) } - fn get_lower_bounds(&self) -> Result, PywrError> { - match self.variable { - Some(variable) => Ok(vec![variable.lower_bound()]), - None => Err(PywrError::ParameterVariableNotActive), - } + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError> { + let activation_function = downcast_variable_config_ref::(variable_config); + Ok(vec![activation_function.lower_bound()]) } - fn get_upper_bounds(&self) -> Result, PywrError> { - match self.variable { - Some(variable) => Ok(vec![variable.upper_bound()]), - None => Err(PywrError::ParameterVariableNotActive), - } + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError> { + let activation_function = downcast_variable_config_ref::(variable_config); + Ok(vec![activation_function.upper_bound()]) + } +} + +#[cfg(test)] +mod tests { + use crate::parameters::{ActivationFunction, ConstantParameter, Parameter, VariableParameter}; + use crate::test_utils::default_domain; + use float_cmp::assert_approx_eq; + + #[test] + fn test_variable_api() { + let domain = default_domain(); + + let var = ActivationFunction::Unit { min: 0.0, max: 2.0 }; + let p = ConstantParameter::new("test", 1.0); + let mut state = p + .setup( + &domain.time().timesteps(), + domain.scenarios().indices().first().unwrap(), + ) + .unwrap(); + + // No value set initially + assert_eq!(p.get_variables(&state), None); + + // Update the value via the variable API + p.set_variables(&[2.0], &var, &mut state).unwrap(); + + // Check the parameter returns the new value + assert_approx_eq!(f64, p.value(&state), 2.0); + + assert_approx_eq!(&[f64], &p.get_variables(&state).unwrap(), &[2.0]); } } diff --git a/pywr-core/src/parameters/delay.rs b/pywr-core/src/parameters/delay.rs index 57871a8b..6ca3dc85 100644 --- a/pywr-core/src/parameters/delay.rs +++ b/pywr-core/src/parameters/delay.rs @@ -1,6 +1,6 @@ use crate::metric::Metric; use crate::network::Network; -use crate::parameters::{downcast_internal_state, Parameter, ParameterMeta}; +use crate::parameters::{downcast_internal_state_mut, Parameter, ParameterMeta}; use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; use crate::timestep::Timestep; @@ -53,7 +53,7 @@ impl Parameter for DelayParameter { internal_state: &mut Option>, ) -> Result { // Downcast the internal state to the correct type - let memory = downcast_internal_state::>(internal_state); + let memory = downcast_internal_state_mut::>(internal_state); // Take the oldest value from the queue // It should be guaranteed that the internal memory/queue has self.delay number of values @@ -73,7 +73,7 @@ impl Parameter for DelayParameter { internal_state: &mut Option>, ) -> Result<(), PywrError> { // Downcast the internal state to the correct type - let memory = downcast_internal_state::>(internal_state); + let memory = downcast_internal_state_mut::>(internal_state); // Get today's value from the metric let value = self.metric.get_value(model, state)?; diff --git a/pywr-core/src/parameters/discount_factor.rs b/pywr-core/src/parameters/discount_factor.rs index 403ac3e8..4994ea91 100644 --- a/pywr-core/src/parameters/discount_factor.rs +++ b/pywr-core/src/parameters/discount_factor.rs @@ -5,6 +5,7 @@ use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; use crate::timestep::Timestep; use crate::PywrError; +use chrono::Datelike; use std::any::Any; pub struct DiscountFactorParameter { diff --git a/pywr-core/src/parameters/mod.rs b/pywr-core/src/parameters/mod.rs index 53942cdd..10f4af27 100644 --- a/pywr-core/src/parameters/mod.rs +++ b/pywr-core/src/parameters/mod.rs @@ -53,7 +53,8 @@ pub use offset::OffsetParameter; pub use polynomial::Polynomial1DParameter; pub use profiles::{ DailyProfileParameter, MonthlyInterpDay, MonthlyProfileParameter, RadialBasisFunction, RbfProfileParameter, - RbfProfileVariableConfig, UniformDrawdownProfileParameter, + RbfProfileVariableConfig, UniformDrawdownProfileParameter, WeeklyInterpDay, WeeklyProfileError, + WeeklyProfileParameter, WeeklyProfileValues, }; pub use py::PyParameter; use std::fmt; @@ -149,7 +150,7 @@ impl ParameterMeta { /// Helper function to downcast to internal parameter state and print a helpful panic /// message if this fails. -pub fn downcast_internal_state(internal_state: &mut Option>) -> &mut T { +pub fn downcast_internal_state_mut(internal_state: &mut Option>) -> &mut T { // Downcast the internal state to the correct type match internal_state { Some(internal) => match internal.as_mut().as_any_mut().downcast_mut::() { @@ -160,6 +161,45 @@ pub fn downcast_internal_state(internal_state: &mut Option(internal_state: &Option>) -> &T { + // Downcast the internal state to the correct type + match internal_state { + Some(internal) => match internal.as_ref().as_any().downcast_ref::() { + Some(pa) => pa, + None => panic!("Internal state did not downcast to the correct type! :("), + }, + None => panic!("No internal state defined when one was expected! :("), + } +} + +pub trait VariableConfig: Any + Send { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl VariableConfig for T +where + T: Any + Send, +{ + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +/// Helper function to downcast to variable config and print a helpful panic message if this fails. +pub fn downcast_variable_config_ref(variable_config: &dyn VariableConfig) -> &T { + // Downcast the internal state to the correct type + match variable_config.as_any().downcast_ref::() { + Some(pa) => pa, + None => panic!("Variable config did not downcast to the correct type! :("), + } +} + // TODO It might be possible to make these three traits into a single generic trait pub trait Parameter: Send + Sync { fn as_any_mut(&mut self) -> &mut dyn Any; @@ -211,14 +251,6 @@ pub trait Parameter: Send + Sync { self.as_f64_variable().is_some() } - /// Is this parameter an active variable - fn is_f64_variable_active(&self) -> bool { - match self.as_f64_variable() { - Some(var) => var.is_active(), - None => false, - } - } - /// Return the parameter as a [`VariableParameter`] if it supports being a variable. fn as_u32_variable(&self) -> Option<&dyn VariableParameter> { None @@ -233,14 +265,6 @@ pub trait Parameter: Send + Sync { fn can_be_u32_variable(&self) -> bool { self.as_u32_variable().is_some() } - - /// Is this parameter an active variable - fn is_u32_variable_active(&self) -> bool { - match self.as_u32_variable() { - Some(var) => var.is_active(), - None => false, - } - } } pub trait IndexParameter: Send + Sync { @@ -340,30 +364,41 @@ pub enum ParameterType { /// such as multi-objective evolutionary algorithms. The trait is generic to the type of /// the variable values being optimised but these will typically by `f64` and `u32`. pub trait VariableParameter { - /// Is this variable activated (i.e. should be used in optimisation) - fn is_active(&self) -> bool; + fn meta(&self) -> &ParameterMeta; + fn name(&self) -> &str { + self.meta().name.as_str() + } + /// Return the number of variables required - fn size(&self) -> usize; - /// Apply new variable values to the parameter - fn set_variables(&mut self, values: &[T]) -> Result<(), PywrError>; + fn size(&self, variable_config: &dyn VariableConfig) -> usize; + /// Apply new variable values to the parameter's state + fn set_variables( + &self, + values: &[T], + variable_config: &dyn VariableConfig, + internal_state: &mut Option>, + ) -> Result<(), PywrError>; /// Get the current variable values - fn get_variables(&self) -> Vec; + fn get_variables(&self, internal_state: &Option>) -> Option>; /// Get variable lower bounds - fn get_lower_bounds(&self) -> Result, PywrError>; + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError>; /// Get variable upper bounds - fn get_upper_bounds(&self) -> Result, PywrError>; + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError>; } #[cfg(test)] mod tests { - use crate::timestep::Timestepper; - use time::macros::date; + use crate::timestep::{TimestepDuration, Timestepper}; + use chrono::NaiveDateTime; // TODO tests need re-enabling #[allow(dead_code)] fn default_timestepper() -> Timestepper { - Timestepper::new(date!(2020 - 01 - 01), date!(2020 - 01 - 15), 1) + let start = NaiveDateTime::parse_from_str("2020-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let end = NaiveDateTime::parse_from_str("2020-01-15 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let duration = TimestepDuration::Days(1); + Timestepper::new(start, end, duration) } // #[test] diff --git a/pywr-core/src/parameters/offset.rs b/pywr-core/src/parameters/offset.rs index d538dbe5..65c8c1af 100644 --- a/pywr-core/src/parameters/offset.rs +++ b/pywr-core/src/parameters/offset.rs @@ -1,6 +1,9 @@ use crate::metric::Metric; use crate::network::Network; -use crate::parameters::{ActivationFunction, Parameter, ParameterMeta, VariableParameter}; +use crate::parameters::{ + downcast_internal_state_mut, downcast_internal_state_ref, downcast_variable_config_ref, ActivationFunction, + Parameter, ParameterMeta, VariableConfig, VariableParameter, +}; use crate::scenario::ScenarioIndex; use std::any::Any; @@ -12,16 +15,28 @@ pub struct OffsetParameter { meta: ParameterMeta, metric: Metric, offset: f64, - variable: Option, } +// We store this internal value as an Option so that it can be updated by the variable API +type InternalValue = Option; + impl OffsetParameter { - pub fn new(name: &str, metric: Metric, offset: f64, variable: Option) -> Self { + pub fn new(name: &str, metric: Metric, offset: f64) -> Self { Self { meta: ParameterMeta::new(name), metric, offset, - variable, + } + } + + /// Return the current value. + /// + /// If the internal state is None, the value is returned directly. Otherwise, the internal value must + /// have come from the variable API and is passed through the activation function. + fn offset(&self, internal_state: &Option>) -> f64 { + match downcast_internal_state_ref::(internal_state) { + Some(value) => *value, + None => self.offset, } } } @@ -39,11 +54,12 @@ impl Parameter for OffsetParameter { _scenario_index: &ScenarioIndex, model: &Network, state: &State, - _internal_state: &mut Option>, + internal_state: &mut Option>, ) -> Result { + let offset = self.offset(internal_state); // Current value let x = self.metric.get_value(model, state)?; - Ok(x + self.offset) + Ok(x + offset) } fn as_f64_variable(&self) -> Option<&dyn VariableParameter> { Some(self) @@ -55,39 +71,44 @@ impl Parameter for OffsetParameter { } impl VariableParameter for OffsetParameter { - fn is_active(&self) -> bool { - self.variable.is_some() + fn meta(&self) -> &ParameterMeta { + &self.meta } - fn size(&self) -> usize { + fn size(&self, _variable_config: &dyn VariableConfig) -> usize { 1 } - fn set_variables(&mut self, values: &[f64]) -> Result<(), PywrError> { + fn set_variables( + &self, + values: &[f64], + variable_config: &dyn VariableConfig, + internal_state: &mut Option>, + ) -> Result<(), PywrError> { + let activation_function = downcast_variable_config_ref::(variable_config); + if values.len() == 1 { - let variable = self.variable.ok_or(PywrError::ParameterVariableNotActive)?; - self.offset = variable.apply(values[0]); + let value = downcast_internal_state_mut::(internal_state); + *value = Some(activation_function.apply(values[0])); Ok(()) } else { Err(PywrError::ParameterVariableValuesIncorrectLength) } } - fn get_variables(&self) -> Vec { - vec![self.offset] + fn get_variables(&self, internal_state: &Option>) -> Option> { + downcast_internal_state_ref::(internal_state) + .as_ref() + .map(|value| vec![*value]) } - fn get_lower_bounds(&self) -> Result, PywrError> { - match self.variable { - Some(variable) => Ok(vec![variable.lower_bound()]), - None => Err(PywrError::ParameterVariableNotActive), - } + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError> { + let activation_function = downcast_variable_config_ref::(variable_config); + Ok(vec![activation_function.lower_bound()]) } - fn get_upper_bounds(&self) -> Result, PywrError> { - match self.variable { - Some(variable) => Ok(vec![variable.upper_bound()]), - None => Err(PywrError::ParameterVariableNotActive), - } + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError> { + let activation_function = downcast_variable_config_ref::(variable_config); + Ok(vec![activation_function.upper_bound()]) } } diff --git a/pywr-core/src/parameters/profiles/daily.rs b/pywr-core/src/parameters/profiles/daily.rs index 35640ca8..6f2411d2 100644 --- a/pywr-core/src/parameters/profiles/daily.rs +++ b/pywr-core/src/parameters/profiles/daily.rs @@ -4,6 +4,7 @@ use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; use crate::timestep::Timestep; use crate::PywrError; +use chrono::Datelike; use std::any::Any; pub struct DailyProfileParameter { diff --git a/pywr-core/src/parameters/profiles/mod.rs b/pywr-core/src/parameters/profiles/mod.rs index f138cbb7..cd4aec38 100644 --- a/pywr-core/src/parameters/profiles/mod.rs +++ b/pywr-core/src/parameters/profiles/mod.rs @@ -2,8 +2,10 @@ mod daily; mod monthly; mod rbf; mod uniform_drawdown; +mod weekly; pub use daily::DailyProfileParameter; pub use monthly::{MonthlyInterpDay, MonthlyProfileParameter}; pub use rbf::{RadialBasisFunction, RbfProfileParameter, RbfProfileVariableConfig}; pub use uniform_drawdown::UniformDrawdownProfileParameter; +pub use weekly::{WeeklyInterpDay, WeeklyProfileError, WeeklyProfileParameter, WeeklyProfileValues}; diff --git a/pywr-core/src/parameters/profiles/monthly.rs b/pywr-core/src/parameters/profiles/monthly.rs index b3d33eda..60b49679 100644 --- a/pywr-core/src/parameters/profiles/monthly.rs +++ b/pywr-core/src/parameters/profiles/monthly.rs @@ -4,9 +4,8 @@ use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; use crate::timestep::Timestep; use crate::PywrError; +use chrono::{Datelike, NaiveDateTime}; use std::any::Any; -use time::util::days_in_year_month; -use time::Date; #[derive(Copy, Clone)] pub enum MonthlyInterpDay { @@ -30,10 +29,20 @@ impl MonthlyProfileParameter { } } +fn days_in_year_month(datetime: &NaiveDateTime) -> u32 { + match datetime.month() { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if datetime.date().leap_year() => 29, + 2 => 28, + _ => panic!("Invalid month"), + } +} + /// Interpolate between first_value and last value based on the day of the month. The last /// value is assumed to correspond to the first day of the next month. -fn interpolate_first(date: &Date, first_value: f64, last_value: f64) -> f64 { - let days_in_month = days_in_year_month(date.year(), date.month()); +fn interpolate_first(date: &NaiveDateTime, first_value: f64, last_value: f64) -> f64 { + let days_in_month = days_in_year_month(date); if date.day() <= 1 { first_value @@ -46,8 +55,8 @@ fn interpolate_first(date: &Date, first_value: f64, last_value: f64) -> f64 { /// Interpolate between first_value and last value based on the day of the month. The first /// value is assumed to correspond to the last day of the previous month. -fn interpolate_last(date: &Date, first_value: f64, last_value: f64) -> f64 { - let days_in_month = days_in_year_month(date.year(), date.month()); +fn interpolate_last(date: &NaiveDateTime, first_value: f64, last_value: f64) -> f64 { + let days_in_month = days_in_year_month(date); if date.day() < 1 { first_value @@ -76,13 +85,16 @@ impl Parameter for MonthlyProfileParameter { let v = match &self.interp_day { Some(interp_day) => match interp_day { MonthlyInterpDay::First => { - let first_value = self.values[timestep.date.month() as usize - 1]; - let last_value = self.values[timestep.date.month().next() as usize - 1]; + let next_month0 = (timestep.date.month0() + 1) % 12; + let first_value = self.values[timestep.date.month0() as usize]; + let last_value = self.values[next_month0 as usize]; interpolate_first(×tep.date, first_value, last_value) } MonthlyInterpDay::Last => { - let first_value = self.values[timestep.date.month().previous() as usize - 1]; + let current_month = timestep.date.month(); + let last_month = if current_month == 1 { 12 } else { current_month - 1 }; + let first_value = self.values[last_month as usize - 1]; let last_value = self.values[timestep.date.month() as usize - 1]; interpolate_last(×tep.date, first_value, last_value) diff --git a/pywr-core/src/parameters/profiles/rbf-test.ipynb b/pywr-core/src/parameters/profiles/rbf-test.ipynb new file mode 100644 index 00000000..75215893 --- /dev/null +++ b/pywr-core/src/parameters/profiles/rbf-test.ipynb @@ -0,0 +1,150 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Compute test values for RBF parameter" + ], + "metadata": { + "collapsed": false + }, + "id": "3855dec3a2ad2790" + }, + { + "cell_type": "code", + "execution_count": 2, + "outputs": [], + "source": [ + "from scipy.interpolate import Rbf\n", + "import numpy as np\n", + "from matplotlib import pyplot as plt\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-09-28T10:02:18.482019061Z", + "start_time": "2023-09-28T10:02:17.513207509Z" + } + }, + "id": "9b57e44a7091586b" + }, + { + "cell_type": "code", + "execution_count": 3, + "outputs": [ + { + "data": { + "text/plain": "array([0.99999999, 1.02215444, 1.03704224, 1.04658357, 1.05232959,\n 1.0555025 , 1.05703598, 1.05761412, 1.05770977, 1.05762023,\n 1.0575012 , 1.05739784, 1.05727216, 1.0570282 , 1.0565335 ,\n 1.05563715, 1.05418473, 1.05203042, 1.04904584, 1.04512659,\n 1.04019611, 1.03420771, 1.02714462, 1.0190189 , 1.00986897,\n 0.99975608, 0.98876095, 0.97697989, 0.96451978, 0.9514951 ,\n 0.93802364, 0.92422356, 0.91021058, 0.89609542, 0.88198282,\n 0.86796961, 0.85414519, 0.8405903 , 0.82737825, 0.81457486,\n 0.80224023, 0.79042854, 0.77919009, 0.76857191, 0.75861923,\n 0.74937591, 0.74088519, 0.73319047, 0.72633599, 0.72036607,\n 0.71532606, 0.71126198, 0.70821968, 0.7062455 , 0.70538494,\n 0.70568346, 0.7071849 , 0.70993231, 0.71396743, 0.71933052,\n 0.72606058, 0.73419586, 0.74377345, 0.75483021, 0.76740264,\n 0.78152758, 0.79724185, 0.81458285, 0.83358751, 0.85429299,\n 0.87673482, 0.90094656, 0.926958 , 0.95479321, 0.98446917,\n 1.01599247, 1.04935705, 1.08454095, 1.12150386, 1.16018313,\n 1.20049191, 1.24231544, 1.28550918, 1.32989614, 1.37526651,\n 1.42137569, 1.46794495, 1.51466233, 1.56118419, 1.6071376 ,\n 1.65212512, 1.69572785, 1.7375121 , 1.77703531, 1.81385273,\n 1.84752542, 1.87762766, 1.90375533, 1.92553407, 1.94262687,\n 1.95474147, 1.96163779, 1.96313291, 1.95910686, 1.94950578,\n 1.93434466, 1.91370844, 1.88775047, 1.85669197, 1.82081727,\n 1.78046916, 1.73604268, 1.68797763, 1.63674943, 1.58286071,\n 1.52683076, 1.46918569, 1.41044858, 1.35112887, 1.29171453,\n 1.23266261, 1.17439264, 1.11728046, 1.06165402, 1.00779065,\n 0.95591582, 0.90620394, 0.8587805 , 0.81372578, 0.77108031,\n 0.73085073, 0.69301704, 0.6575401 , 0.62436898, 0.59344848,\n 0.56472532, 0.53815332, 0.51369657, 0.49133094, 0.47104256,\n 0.45282388, 0.43666555, 0.42254569, 0.4104155 , 0.40018055,\n 0.39167888, 0.38465535, 0.37873281, 0.3733805 , 0.36787943])" + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAGdCAYAAADAAnMpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/OQEPoAAAACXBIWXMAAA9hAAAPYQGoP6dpAABSLUlEQVR4nO3deXwU9f3H8dfu5gIkyxkSIEAAueQGgxFRVJBD461UFCjeij+t1LZSFYoX1qrVWpR624qCUgQRGkUUEEQjRxTkJsFwJOGSTQjk2p3fH0MCEQLZkN3Z4/18PPYxk81M9p3RZT+Z72UzDMNARERExCJ2qwOIiIhIeFMxIiIiIpZSMSIiIiKWUjEiIiIillIxIiIiIpZSMSIiIiKWUjEiIiIillIxIiIiIpaKsDpAdXg8Hnbv3k39+vWx2WxWxxEREZFqMAyDgoICmjdvjt1e9f2PoChGdu/eTWJiotUxREREpAZ27NhBy5Ytq/x+UBQj9evXB8xfJjY21uI0IiIiUh35+fkkJiZWfI5XJSiKkfKmmdjYWBUjIiIiQeZ0XSzUgVVEREQspWJERERELOVVMTJlyhTOPfdc6tevT1xcHFdffTWbNm067XkfffQRnTp1IiYmhm7durFgwYIaBxYREZHQ4lUxsmTJEsaNG8e3337LwoULKS0t5bLLLqOwsLDKc7755htuuukmbrvtNtasWcPVV1/N1Vdfzbp16844vIiIiAQ/m2EYRk1P3rt3L3FxcSxZsoQLL7zwpMeMGDGCwsJCPv3004rnzjvvPHr27Mm0adOq9Tr5+fk4nU5cLpc6sIqIiASJ6n5+n1GfEZfLBUCjRo2qPGbFihUMGjSo0nNDhgxhxYoVVZ5TXFxMfn5+pYeIiIiEphoXIx6Ph9/97nf079+frl27Vnlcbm4uzZo1q/Rcs2bNyM3NrfKcKVOm4HQ6Kx6a8ExERCR01bgYGTduHOvWrWPGjBm1mQeACRMm4HK5Kh47duyo9dcQERGRwFCjSc/uu+8+Pv30U5YuXXrK6V0B4uPjycvLq/RcXl4e8fHxVZ4THR1NdHR0TaKJiIhIkPHqzohhGNx33318/PHHfPnllyQlJZ32nJSUFBYtWlTpuYULF5KSkuJdUhEREQlJXt0ZGTduHO+//z5z586lfv36Ff0+nE4nderUAWD06NG0aNGCKVOmAPDAAw9w0UUX8fzzz3P55ZczY8YMVq5cyWuvvVbLv4qIiIgEI6/ujLz66qu4XC4GDhxIQkJCxWPmzJkVx2RnZ5OTk1Px9fnnn8/777/Pa6+9Ro8ePZg1axZz5sw5ZadXERERCR9nNM+Iv2ieERGRKuTvht0ZULAbCnIhsg60vRgSeoJdK36Itar7+R0Uq/aKiMiv7N0My16AHz8Ew135e4seh7qNoXMqXDIR6jW2JqNINakYEREJJiWFMP/38MMM4OiN7WbdoEErqB8Ph/Igcwkc3g+r3oGN8+HyF6DLlVamFjklFSMiIsEifze8PwJyfzS/7ng5DPg9tOxT+Th3KWxfBmkTYO8G+HAUdLsBrvwnRMb4P7fIaahBUUQkGOxeA69fYhYidRvD2P/BTe+fWIgAOCKh3cVw1xKzWLE5YO1HMOtWs1ARCTAqRkREAl3uOnjnCijIgaad4I4vofX5pz8vIhounQijZoMjGjbNhzn3gsfj+8wiXlAxIiISyA7tgQ9+AyWHoHV/uO1zaNjGu5/RdiDc+G+wR8DaD2H+eAj8gZQSRlSMiIgEqtIimHEzuHZAo3Yw4j2IcdbsZ3UcCte+Bthg1duw5j+1GlXkTKgYEREJRIYBn/wf7EyHmAYw8kOo2+jMfmbX62DQJHP/fw/DgcwzjilSG1SMiIgEorUfmU0q9giziaVJ+9r5ueffbzb3lBZizL6bFVv2MDdjFyu27cftUdONWENDe0VEAk1BHvzvj+b+RQ9D24tq72fbHXDNNMr+eR4RO7/j63ce5RX3VQAkOGOYlNqFoV0Tau/1RKpBd0ZERAKJYcCC38ORXyC+O1zwu1p/ibSdkfzp8CgAHoyYRXvbTgByXUXc895q0tblnOp0kVqnYkREJJCsnwMb5pnNM1dNNecMqUVuj8Hkeev5r2cAC919iLS5mRDxAVAxnyuT561Xk434lYoREZFAceQgzH/I3L9gPCR0r/WXSM86QI6rCLDxdNlISg0HlzrWkGL/CTALkhxXEelZB2r9tUWqomJERCRQLHsBDu+DJh3hwj/45CX2FBRV7GcZCUx3XwrAIxHTseE56XEivqZiREQkEBzMhm+nmfuXPQERUT55mbj6ldem+UfZteQbdehq387V9uVVHifiSypGREQs4vYYrNi2n7kZu9g79zFwF0ObAXD2ZT57zeSkRiQ4Y7Ad/foAsbxaZo6meSjyQ2IoIcEZQ3LSGc5pIuIFFSMiIhZIW5fDBX/9kpte/5bXZn5M06w5AHzT7gGw2U598hlw2G1MSu0CUFGQvOUeyi6jMS1s+7nBsYRJqV1w2H2XQeTXVIyIiPhZ2roc7nlv9dGOpAZ/jngfgDnu87l5fonPh9YO7ZrAq7f0Jt5pNsUUE8W/yq4AYEKDLxjaualPX1/k1zTpmYiIH5UPrS0fOHuefQP9HT9RbETwXNkIwBxaO7hLvE/vTgztmsDgLvGkZx1gT0ER8THdMebOo27hDtgw15w6XsRPdGdERMSPjg2tNd3rmAvATPfF7DSa+nVorcNuI6VdY67q2YJ+nRKx9bvL/Mbyl7Sqr/iVihERET86fshsd9s2LnSspcyw85r7iiqP85tz74CIOpDzA2Qu9v/rS9hSMSIi4kfHD5kdF2HeFZnr6c9Oo2mVx/lNvcbQe7S5v/wl/7++hC0VIyIiflQ+tLaDbSdDHCvxGDZeKbuy4vs2sHZobco4sDkg8yvIXWtNBgk7KkZERPyofGjtPRGfAJDmOZdtRgvg2FBbS4fWNmwNXY4WRyvfsiaDhB0VIyIifja0ZSlXRawAqHRXJN4Zw6u39GZo1wSropn6jDW3P34ExYeszSJhQUN7RUT87fs3sBtujKSLeOSCkewpKCKuvtk0ExCTjSVdCI3awoFMWPdf6DPG6kQS4nRnRETEn0oOw6p3AbD1u7tiaG1Ku8aBUYiAOQNsn9+a+6vesTKJhAkVIyIi/rT2Iyg6CA1aQYchVqepWs+bwR4Ju1ebQ31FfEjFiIiIvxgGpL9m7p97B9gd1uY5lXpNoHOqua+7I+JjKkZERPzl528gbx1E1oXeo6xOc3p91ZFV/EPFiIiIv3w3zdx2vxHqNLQ2S3W0GQCN2kFJAayfa3UaCWFeFyNLly4lNTWV5s2bY7PZmDNnzmnPmT59Oj169KBu3bokJCRw6623sn///prkFREJTq5dsHG+uZ98p7VZqstmgx43mftrP7Q2i4Q0r4uRwsJCevTowdSpU6t1/PLlyxk9ejS33XYbP/30Ex999BHp6enccccdXocVEQlaGdPBcEPr/tDsHKvTVF+3681t1lIoyLU2i4Qsr+cZGTZsGMOGDav28StWrKBNmzbcf//9ACQlJXHXXXfx17/+1duXFhEJTh4PrPmPud87yObsaJQELZNhZ7o550jKOKsTSQjyeZ+RlJQUduzYwYIFCzAMg7y8PGbNmsXw4cOrPKe4uJj8/PxKDxGRoJW1BA5mQ7Tz2FTrwaT7jeb2RzXViG/4vBjp378/06dPZ8SIEURFRREfH4/T6TxlM8+UKVNwOp0Vj8TERF/HFBHxndX/Nrfdb4TIOtZmqYlzrgV7BORkwN7NVqeREOTzYmT9+vU88MADTJw4kVWrVpGWlsb27du5++67qzxnwoQJuFyuiseOHTt8HVNExDcK98PGT8393qOtzVJT9RpDu0vNfXVkFR/w+do0U6ZMoX///vzhD38AoHv37tSrV48BAwbw5JNPkpBw4oJQ0dHRREdH+zqaiIjv/TgT3CWQ0AMSuludpua63whbPjNnkL34EXOkjUgt8fmdkcOHD2O3V34Zh8OcddAwDF+/vIiIdQzjWBNNsN4VKddxGETWg1+2w87vrU4jIcbrYuTQoUNkZGSQkZEBQFZWFhkZGWRnZwNmE8vo0cfedKmpqcyePZtXX32VzMxMli9fzv33309ycjLNmzevnd9CRCQQ7VoNezdARB3oer3Vac5MVD3odLm5rwnQpJZ5XYysXLmSXr160atXLwDGjx9Pr169mDhxIgA5OTkVhQnAb3/7W1544QX++c9/0rVrV2644QY6duzI7Nmza+lXEBEJUD98YG47XwF1GlgapVaUjwRa/4l510ekltiMIGgryc/Px+l04nK5iI2NtTqOiMjplZXA8x3hyAG45b/QfpDVic5cyWH4WzsoPQx3LobmvaxOJAGuup/fWptGRMQXtn5hFiJnNYOkgVanqR1RdY8VVRvmWZtFQoqKERERX/hxhrntdgM4fD5w0X+6XGVu1VQjtUjFiIhIbTvyC2xKM/e7j7A2S207+zJwRMH+LbB3o9VpJESoGBERqW0/zQF3McR1gfhuVqepXTGx0PZic3/9J9ZmkZChYkREpLb9ONPcdh8RmpODlY+q2aBiRGqHihERkdr0y3bIXgHYzP4ioajjcLA5IG8d7N9mdRoJASpGRERq07qjcyglDQBnC2uz+ErdRtDmAnN/0wJrs0hIUDEiIlKbyouRYJ9x9XQ6Dje35R11Rc6AihERkdqydxPkrQV7BHROtTqNb3UYYm6zV5ijh0TOgIoREZHaUn5XpN2lZlNGKGuUBE07geGGrYusTiNBTsWIiEhtMAxY919zv+t11mbxlw5Dze1mNdXImVExIiJSG3LXmhOBRcRAx2FWp/GP8mJky0Jwl1mbRYKaihERkdpQflfk7MvMicHCQWIy1GkIRQdhx3dWp5EgpmJERORMGcZxo2jCpIkGwO4wiy+Azf+zNosENRUjIiJnaudKcGVD1FnHPpzDRUW/kc+szSFBTcWIiMiZWj/H3HYYClF1LY3id+0vNYcy79us2VilxlSMiIicCcM4tkZL+Zot4STGCa3PN/e3fG5tFglaKkZERM5Ezg9wMBsi6kD7QVansUb7weZW841IDakYERE5E+V3Rc4eBFH1rM1ilfIibPsyKD1ibRYJSipGRERqyjBg/dFipPNV1maxUlxnqN8cyo7Az99YnUaCkIoREZGa2rvRnOjMEXVsrZZwZLNB+0vMfTXVSA2oGBERqanyuyJtLw6fic6qUt5Us03FiHhPxYiISE2F8yiaX2s7EGx2827RwR1Wp5Ego2JERKQm9m+DvHXmHBsdh1udxnp1GkLLc8193R0RL6kYERGpifK7Im0GQN1G1mYJFO0uNbdbv7A2hwQdFSMiIjWxXk00JyjvN5K5BNyl1maRoKJiRETEWwd3wO7VgA06XWF1msDRvCfUaQTF+bDze6vTSBBRMSIi4q0N88xt6/PhrDhrswQSuwPaXWzub/vK2iwSVFSMiIh4q7y/SGc10Zyg7dFiJHOxpTEkuKgYERHxRkEeZH9r7ndOtTZLIGo70NzuWgVFLkujSPBQMSIi4o2N8wADWvQFZwur0wSeBonQqB0YbnOtGpFq8LoYWbp0KampqTRv3hybzcacOXNOe05xcTGPPPIIrVu3Jjo6mjZt2vDWW2/VJK+IiLU0iub02qmpRrwT4e0JhYWF9OjRg1tvvZVrr722WufceOON5OXl8eabb9K+fXtycnLweDxehxURsdThA8f+2ld/kaq1HQjfv6FiRKrN62Jk2LBhDBs2rNrHp6WlsWTJEjIzM2nUyJwYqE2bNt6+rIiI9TYtMJsf4rtBoySr0wSuNheYU8Pv2wyuXWrOktPyeZ+RTz75hL59+/Lss8/SokULOnTowEMPPcSRI0eqPKe4uJj8/PxKDxERy21cYG47qePqKdVpCM17mfu6OyLV4PNiJDMzk2XLlrFu3To+/vhjXnzxRWbNmsW9995b5TlTpkzB6XRWPBITE30dU0Tk1EoOw7Yvzf1OWovmtDTEV7zg82LE4/Fgs9mYPn06ycnJDB8+nBdeeIF33323yrsjEyZMwOVyVTx27NAKkCJiscyvoOwIOFtBs65Wpwl85UN8MxeDYViZRIKAz4uRhIQEWrRogdPprHiuc+fOGIbBzp07T3pOdHQ0sbGxlR4iIpaqaKIZDjabtVmCQWIyRNSBwj2wZ4PVaSTA+bwY6d+/P7t37+bQoUMVz23evBm73U7Lli19/fIiImfO44bNaeZ+RzXRVEtEtDldPqipRk7L62Lk0KFDZGRkkJGRAUBWVhYZGRlkZ2cDZhPL6NGjK44fOXIkjRs3ZuzYsaxfv56lS5fyhz/8gVtvvZU6derUzm8hIuJLO9Lh8D6IaXDsA1ZOL+lCc7v9a2tzSMDzuhhZuXIlvXr1olcvs6f0+PHj6dWrFxMnTgQgJyenojABOOuss1i4cCEHDx6kb9++3HzzzaSmpvKPf/yjln4FEREf2zTf3HYYAo5Ia7MEk6QB5vbn5ebdJZEq2Awj8HsW5efn43Q6cblc6j8iIv5lGPBybziQCTe8C+dcbXWi4OEug2eToDgf7lwCzXtanUj8rLqf31qbRkTkVPZuMgsRRxS0v9TqNMHFEQGtUsx9NdXIKagYERE5lfImmqSLILq+tVmCUXlTTZaKEamaihERkVOpGNJ7ubU5glWbo8VI9gqz2UbkJFSMiIhUpSAXdq009ztWf00uOU58N4hxmv1Gcn+wOo0EKBUjIiJV2XT0rkiLvlA/3toswcrugNb9zX011UgVVIyIiFTl+FlXpebKm2q2L7M2hwQsFSMiIidTXABZS8z9juovckaSju83UmptFglIKkZERE5m6yJwl0CjttC0o9VpglvcOVCnIZQcgt0ZVqeRAKRiRETkZDYdN4pGC+OdGbv9WL+R7UutzSIBScWIiMivuUuPWxhPTTS1omKdGvUbkROpGBER+bWfv4EiF9RtAonJVqcJDRXzjXwLZSXWZpGAo2JEROTXyptoOgw1h6bKmWvaCeo2htLDsHu11WkkwKgYERE5nmFoSK8v2O3Q5gJzX+vUyK+oGBEROV7eOnBlQ0QdaHux1WlCSxutUyMnp2JEROR4G48ujNfuEoiqa22WUFPeiXXHd1BWbG0WCSgqRkREjldejKiJpvY16QD14qCsCHatsjqNBBAVIyIi5Q7ugNwfwWY3O69K7bLZjvUbUVONHEfFiIhIuU3/M7eJ/aBeE2uzhKryqeHViVWOo2JERKTcpvImGk105jPlnVh3pENpkbVZJGCoGBERAThy8NjsoB3VX8RnGreHs+LBXQw7v7c6jQQIFSMiIgBbFoKnzJycq3E7q9OELptNTTVyAhUjIiJwrIlGd0V8r3zRvJ+/sTaHBAwVIyIiZcWw5QtzX/1FfK98RM3O7zXfiAAqRkREzOaCkgKzL0Pz3lanCX2N22u+EalExYiIhC23x2DFtv1kLfsQAE/HYeYaKuJbNhu0Pt/c/3m5tVkkIOhdJyJhKW1dDhf89UtGvv4NdbI+B+D3P7QgbV2OxcnCRMWieSpGRMWIiIShtHU53PPeanJcRXSzZRFv+4VDRgwLDp3NPe+tVkHiD+WdWHekg7vU2ixiORUjIhJW3B6DyfPWYxz9erDD7LOwxNOdYiIBmDxvPW6PUcVPkFrRtBPUaQilhbA7w+o0YjEVIyISVtKzDpDjOjbz52C7WYwsdPcFwAByXEWkZx2wIl74sNuPG+Krpppwp2JERMLKnoJjhUgrWx6d7DsoM+x86elZ5XHiI0eLkV82fMXcjF2s2LZfd6TCVITVAURE/CmufkzF/mD7SgC+83Qmn7OqPE58Y3lZR/oDjp3f8eC21Xiwk+CMYVJqF4Z2TbA6nviR13dGli5dSmpqKs2bN8dmszFnzpxqn7t8+XIiIiLo2bOnty8rIlIrkpMakeCMwQZcdrS/yOeevhXftwEJzhiSkxpZEzBMpK3LYfT8w+QbdYm1HaGz7WcAcl1F6kQchrwuRgoLC+nRowdTp0716ryDBw8yevRoLr30Um9fUkSk1jjsNialdqExLs61bQJgobsPYBYiAJNSu+Cw26r4CXKmyjsRu7HzvacjAOfZNwBUdCxWJ+Lw4nUxMmzYMJ588kmuueYar867++67GTlyJCkpKd6+pIhIrRraNYG3++/HbjP40ZPEbpoAEO+M4dVbequJwMeO70T8nacTAP2OFiOgTsThyC99Rt5++20yMzN57733ePLJJ097fHFxMcXFx9YryM/P92U8EQlD3fLNFWMb9LqGl5J6ElffbJrRHRHfO75zcLqnMwDn2jdhw4Nx3N/I6kQcPnxejGzZsoWHH36Yr7/+moiI6r3clClTmDx5so+TiUjYKi6AzMUAtOp/I63iWlibJ8wc3zl4ndGGQiOahrZDdLDtZJPR6qTHSWjz6dBet9vNyJEjmTx5Mh06dKj2eRMmTMDlclU8duzY4cOUIhJ2ti4CdzE0amtOviV+dXwn4jIiWOUxPx/Km2rUiTj8+LQYKSgoYOXKldx3331EREQQERHB448/zg8//EBERARffvnlSc+Ljo4mNja20kNEpNZs/NTcdrrcXLRN/Kq8EzGYhcd3R5tq+tk3qBNxmPJpM01sbCxr166t9Nwrr7zCl19+yaxZs0hKSvLly4uInKisBDabC+PRKdXaLGFsaNcEXr2lN5Pnree7fPPuVLJ9I/Gx0Uy68hx1Ig4zXhcjhw4dYuvWrRVfZ2VlkZGRQaNGjWjVqhUTJkxg165d/Pvf/8Zut9O1a9dK58fFxRETE3PC8yIifrH9ayh2Qb04aHmu1WnC2tCuCQzuEs/3WzvjnvFXmnryWXZ7Io44FSLhxutmmpUrV9KrVy969eoFwPjx4+nVqxcTJ04EICcnh+zs7NpNKSJSWzbON7edhpvro4ilHHYb53VojqNVsvl1ttapCUc2wzACflaZ/Px8nE4nLpdL/UdEpOY8HnihMxzKhZtnwdmDrU4k5b6aAkuega7Xw/VvWp1Gakl1P7/1Z4GIhI/dq81CJKo+JF1odRo5XpvyFXy/gcD/G1lqmYoREQkf5aNozh4MEdHWZpHKWp4L9kgo2A2/ZFmdRvxMxYiIhI8Nxw3plcASWQdamGsEsV39RsKNihERCQ97N8P+LeZf32dfZnUaOZnjm2okrKgYEZHwsHGeuW17EcSoI3xAal1ejCyzNof4nYoREQkPFUN61UQTsBL7gc0BB7PhoJYBCScqRkQk9OXvhl2rABt0VDESsKLPguY9zf2f1W8knKgYEZHQV35XpOW5UL+ZtVnk1CqaalSMhBMVIyIS+tbPNbedr7A2h5xemwvMrUbUhBUVIyIS2g7tPfZXdperrM0ip5fYD7DBgW1QkGt1GvETFSMiEto2fgqGBxJ6QsM2VqeR06nTAOK7mftqqgkbKkZEJLSVN9HorkjwUFNN2FExIiKh6/AByFpq7qsYCR6tzze3ujMSNlSMiEjo2jgfDDc06waN21mdRqqr1dFiZO9GKNxnbRbxCxUjIhK61EQTnOo1hrgu5r6mhg8LKkZEJDQd+QUyF5v7KkaCj+YbCSsqRkQkNG1KA08pNO0MTTtYnUa8Vd5vRJ1Yw4KKEREJTRs+Mbe6KxKcyu+M5K0z73JJSFMxIiKhpygfti4y91WMBKf6zaDx2YAB2d9anUZ8TMWIiISeLZ+Du9j8MIvrbHUaqamKpppl1uYQn1MxIiKhZ/0cc9vlKrDZLI0iZ6B88jN1Yg15KkZEJLQUH4ItC819NdEEt/J+Izk/QHGBtVnEp1SMiEho2boQyoqgYdKxNU4kODlbmOsJGR7I/s7qNOJDKkZEJLQcP9GZmmiCX8V8I+o3EspUjIhI6Cg5DJs/N/fVRBMaKooRzcQaylSMiEjo2LYISgvB2Qqa97I6jdSGNkeLkV2rzWJTQpKKEREJHev+a267XKkmmlDRoDXEtjBn092ZbnUa8REVIyISGooLzCngAbpdb20WqT0227GmGk0NH7JUjIhIaNi4AMqOQKN2kNDT6jRSm9qo30ioUzEiIqFh3Sxz2+0GNdGEmvI7Izu/h9Iia7OIT6gYEZHgV7jv2Fo0aqIJPY3bQ704c4r/XausTiM+4HUxsnTpUlJTU2nevDk2m405c+ac8vjZs2czePBgmjZtSmxsLCkpKXz22Wc1zSsicqL1c8BwQ0IPaHK21WmkttlsaqoJcV4XI4WFhfTo0YOpU6dW6/ilS5cyePBgFixYwKpVq7j44otJTU1lzZo1XocVETmptUdH0XS7wdoc4jua/CykRXh7wrBhwxg2bFi1j3/xxRcrff30008zd+5c5s2bR69emgdARM7QwR2Q/Q1gg3OutTqN+Ep5MbIjHdyl4Ii0No/UKq+LkTPl8XgoKCigUaNGVR5TXFxMcXFxxdf5+fn+iCYiwein2ea2dX9zLRMJTU07QZ1GcOQA7F4DiclWJ5Ja5PcOrM899xyHDh3ixhtvrPKYKVOm4HQ6Kx6JiYl+TCgiQWXtR+a223XW5hDfstuh9fnm/s+abyTU+LUYef/995k8eTIffvghcXFxVR43YcIEXC5XxWPHjh1+TCkiQWPvJshdC/YI6HK11WnE1zT5WcjyWzPNjBkzuP322/noo48YNGjQKY+Njo4mOjraT8lEJGitPTq3SLtLoW7VTb8SIspH1GR/C+4ycPi9p4H4iF/ujHzwwQeMHTuWDz74gMsvv9wfLykioc4wjmui0SiasNCsK0Q7oaQA8tZanUZqkdfFyKFDh8jIyCAjIwOArKwsMjIyyM7OBswmltGjR1cc//777zN69Gief/55+vXrR25uLrm5ubhcrtr5DUQkPO1eDb9kQWRd6Fj9EX4SxOwOaHWeua+mmpDidTGycuVKevXqVTEsd/z48fTq1YuJEycCkJOTU1GYALz22muUlZUxbtw4EhISKh4PPPBALf0KIhKWyptoOg6D6LOszSL+UzH5mYqRUOJ1g9vAgQMxDKPK77/zzjuVvl68eLG3LyEicmoeN6w7OqRXTTThpfUF5vbnb8DjMUfZSNDTf0URCT7bl8GhXIhpYHZelfCR0AMi60HRQdiz3uo0UktUjIhI8Fn7obntciVERFmbRfzLEQGt+pn7aqoJGSpGRCS4lByGn+aa+91/Y20WsUbFfCNapyZUqBgRkeCy8VNzaGeD1tAqxeo0YoU2x/UbOUUfRgkeKkZEJLhkvG9ue9ykzovhqnkviIiBw/tg32ar00gt0DtZRIKHaxdkLjb3e6iJJmxFREPLc819NdWEBBUjIhI8fpwBGNDqfGiUZHUasVJFU406sYYCFSMiEhwMAzI+MPd73mRtFrFeeSdW9RsJCSpGRCQ47FoF+7dARB2t0CvQsi84oqAgBw5kWp1GzpCKEREJDhnTzW3nKyAm1tosYr3IOtCij7mvppqgp2JERAJfyeFja9H0vNnaLBI4NN9IyFAxIiKBb8MnUJwPDVpB0kVWp5FAkTTA3GYtVb+RIKdiREQC3+p/m9teozW3iByT2A8c0Wa/kX1brE4jZ0DvahEJbPu2mn0CbHboOdLqNBJIIuscW6emfP4ZCUoqRkQksK35j7ltPwicLazNIoGn7UBzm7XE0hhyZlSMiEjgcpfCD0fnFuk1ytosEpiSBprb7V+Dx21lEjkDKkZEJHBt+RwO5UG9ptBhqNVpJBA17wnRTihyQU6G1WmkhlSMiEjgWvWuue3xG4iIsjaLBCa749jU8Oo3ErRUjFjI7TFYsW0/czN2sWLbftweDU0TqXAw27wzAtBnrLVZJLCV9xvJVL+RYBVhdYBwlbYuh8nz1pPjKqp4LsEZw6TULgztmmBhMpEAseodwDA/aBq3sziMBLS2R+ee2fEdlBZBZIy1ecRrujNigbR1Odzz3upKhQhArquIe95bTdq6HIuSiQSIspJjc4v0vdXaLBL4mnSAs+KhrMgsSCToqBjxM7fHYPK89ZysQab8ucnz1qvJRsLbxk+hcK/5AdNxuNVpJNDZbMfujmiIb1BSM01NuEshbx3k/ACuXVCwGw7tgdIj5vfcJcdtS8w3is0BNjvFZQZvHSnCHWXHgw0Px7Zu7BjYcB+2U/Da8zSoF2NO6hN1FkTVMx/R9c1t3SZQPx7qJ0CDRPM4kVCx8i1z23s0OCKtzSLBIeki+HGm2Yn10olWpxEvqRipriIXrP0I1s2GXauh7EiNfkxdoHN17kflevmDnYlmu3rcOdCit/lomGQWQiLBZO9mc84Imx36jLE6jQSL8jsju9fAkYNQp4GVacRLKkZO55ftsPQ5WPdfKD187PkYp7l8dcM2ENvcvJ0cVRccUUcfkebWHgkYYHjA4+anXQeZsuAn7EfvidgxcBzd2ir2Pdx/cVvOblrXLHqKD0FJIZQcMh/Fh8xb2AW55poMxfng2mE+jh/adlYzaHfJ0celUK9xpV/N7TFIzzrAnoIi4urHkJzUCIddxYtYrPyuSIeh4GxpbRYJHs6W0Lg97D+6fECny61OJF5QMVIVjwe+fwO+mHSsCGnSEfr81pyWunH7Gi3Y1am1wbav65LrKjppvxEbEO+Moe2ll0B1CgPDgMP7zTfgvs1m09Gu1WYz0qE8c/bKHz4wm4naDoSu10HnK0jbelijeSTwFBdAxnRzv+9t1maR4JN0kflvYeYSFSNBRsXIybh2wuy74Odl5tet+8Mlj0Gr88642cNhtzEptQv3vLcaG1QqSMp/8qTULtW/Q2GzQb0m5qPVeceeLz3aq3zbl7B1EeSthW2LYNsiyj79Pb8Un0cD92Xk0LrilPLRPK/e0lsFiVgj4wPzTl/js807eiLeaHsRrHxTnViDkM0wjIAftpGfn4/T6cTlchEbG+vbF3PtgreHwcGfIbIeDJ5s/oVWy8uW+32ekf3bYN1sjLUfYdu3qeLp7zyd+EfZNSz3dAVsFXdmlv3pEjXZiH95PDD1XPMv2+HPQfIdVieSYHP4ADzbFjBg/EaI1R9VVqvu57eKkeMV5ME7w81/DBsmwajZ0Kitz17Oij4bK7bu4+9vvsPoiIUMsX9PpM1cWGqV52z+XnY9yzzdAPjgjvNIadf4VD9KpHZtWQjTr4foWBi/3hw5JuKtf11krlFzzWvQY4TVacJedT+/1UxTrnA//PsqsxBxJsKYT6BBK5++pMNu8/sH/p5DxaQbnUkv7Uw8+7kr4lNucnxJH/sW3ouawmJ3D54su5k9BUWn/2Eitem7aea21ygVIlJzbY8WI1lLVIwEkbCd9KzSujBb92F8fBfs3WCOihk91+eFiFXi6h+bJjmXxkwuG8OA4hd5q2woJYaDgY4fSIt6mOQNz0BRvoVJJRyUvw8Xfb0Mtn6BgQ2Sb7c6lgSzpKNDfDOXmB38JSh4XYwsXbqU1NRUmjdvjs1mY86cOac9Z/HixfTu3Zvo6Gjat2/PO++8U4OotSdtXQ4X/PVLbnr9Wx6YkcHMt57HtnUhbnsUjPo4pNfBSE5qRIIzhuMbg/bSkMfLRnNZybN85u5LhM1DwqZ/w9R+sHG+ZVkltB3/Ptz52YsALLP3IW23JvCTM9AqxZxWIX8nHMi0Oo1Uk9fFSGFhIT169GDq1KnVOj4rK4vLL7+ciy++mIyMDH73u99x++2389lnn3kdtjb8el2YxriYFGmugfF88TWk7W1oSS5/KR/NA/Dr3ik/GwncXTqe9AFvm31mCnbDjJHw4WizGUuklhz/PmxAATc4lgIwregyrc8kZyaqLrRMNvczv7I2i1Sb18XIsGHDePLJJ7nmmmuqdfy0adNISkri+eefp3Pnztx3331cf/31/P3vf/c67Jk62bowf4l8l4a2Q/zkac3r7svDYl2YoV0TePWW3sQ7K69sGe+M4dVbepN86bVw7wq44EFzfpL1c+HV880hwiJn6Nfvw1GOhdS1FbPO04blnnMArc8kZ6jtQHN7/CSQEtB83oF1xYoVDBo0qNJzQ4YM4Xe/+12V5xQXF1NcXFzxdX5+7fRdSM86UGko7WD7SlId31Jm2Plj6Z2UEkGOq4j0rAMhP5JkaNcEBneJr3o0T2QdGPQXOOca+O8dsG8TvHct9LvHHO4cEW1pfglex78PoylhTMTnALxWdjlgw4CweR+Kj7S7GL56EjKXgrsMHBqrEeh83oE1NzeXZs2aVXquWbNm5Ofnc+TIydd3mTJlCk6ns+KRmJhYK1kqjxAxeDDivwC85r6Cn4ykKo4LXeWjea7q2YKUdo1PPqw4oQfctQSS7zS//u5VeGsoHMz2b1gJGce/v651fE0TWz47jSYs8PSr8jgRrzTvBTENoNgFu1ZZnUaqISBH00yYMAGXy1Xx2LFjR6383ONHkoCNW0om8HrZcF4qu/YUxwmRdWD432Dkh+YbfPdqmDYANn9udTIJQuXvLxsebncsAOCtsmGU/epGrd6HUmN2x7Gmmm1qXg4GPi9G4uPjycvLq/RcXl4esbGx1Klz8l7z0dHRxMbGVnrUhl+PJDlALE+V3UIxUYDZoTPBaTZXyEl0GAJ3fw3Ne0PRQXj/Rlj+kobPiVfK34eD7atpZ88h36jLTPfAiu/rfSi1ov2l5lZ93YKCz4uRlJQUFi2q/D/DwoULSUlJ8fVLn+BUI0lqtC5MOGrQCm5Ng763AgYsnAif3AdlJVYnkyDhsNuYdEVn7oqYB8B77kEUYv5hoveh1Jp2R4uR3avNaeIloHldjBw6dIiMjAwyMjIAc+huRkYG2dlmH4IJEyYwevToiuPvvvtuMjMz+eMf/8jGjRt55ZVX+PDDD3nwwQdr5zfw0ulGkmiBuGqIiIbLX4Bhz4LNDmveg/9coze8VNvQelvoY99CMZG8XTa04nm9D6XWOFtA005geDSqJgh43cV45cqVXHzxxRVfjx8/HoAxY8bwzjvvkJOTU1GYACQlJTF//nwefPBBXnrpJVq2bMkbb7zBkCFDaiF+zZx2JImcns0G/e4y1+75aKy5wvEbl5r9SpqcbXU6CXRL/wZA5Lm/5R+dhup9KL7R7lLYu9HsN9L12tMfL5bRQnly5vLWw/sjwJUNMU648d/HOo+J/Fr2t/DWELBHwv1roEHtjJYTOcHWL+C966B+c3PxRZsKXX+r7ud3QI6mkSDTrAvcsQhangtFLvPN/+NHVqeSQLX0OXPb8yYVIuJbrfuDI9qcTXrvJqvTyCmoGJHacVYcjPkUul4HnjKYfQd895rVqSTQ7F4DWxeafY0usKbfmISRyDrQ+nxzX0N8A5qKEak9kTFw7RtHJ0gz4H9/gMXPaOivHFN+V6TbDWZ/IxFfqxji+4W1OeSUVIxI7bLbzVE2AyeYXy+eAv/7I3g81uYS6+1aDRs/Ne+KDPi91WkkXLQfbG63L4eSQmuzSJVUjEjts9lg4MMw/DnABumvwcd3grvU6mRipa+eMrfdR0DTjtZmkfDRtCM4W4G7GLK+tjqNVEHFiPhO8h1w3Rtgj4C1H8EHN0HpydcjkhD38wrzNrk9Ai76o9VpJJzYbHD20bsjW7SERaBSMSK+1e16uGkGRNQxOy5Ov0G3SsONYcCXT5r7vW5RXxHxv7MvM7dbFqoPW4BSMSK+d/ZgGPUxRNWH7V+bQ3+L8nF7DFZs28/cjF2s2LYft0f/SISkrCXmpHiOKLjwD1ankXCUNMAc4uvK1hDfAOX1DKwiNdI6BUbPgf9cC9krOPjaFdxw6CG25DsqDklwxjAptYumAg8lhgGLHjf3+94KzpbW5pHwFFXPLEi2fmE21cR1sjqR/IrujIj/tOwLYz6hJMpJgwM/8HzRRJwcqvh2rquIe95bTdq6HAtDSq36aTbsWgVRZ2kEjViroqlG/UYCkYoR8St3fA9uNSaxz4iluz2LGVFP0oh8AMobaSbPW68mm1BQVgxf/MXc7/+AOTGeiFXaDzK32SugKN/aLHICFSPiV+lZB1hWEM9vSh5lj9GAzvZsZkQ9QVMOAmZBkuMqIj1LKwAHvfTX4WA2nBUPKeOsTiPhrnE7aNzenCFaq/gGHBUj4ld7CooA2Gq0ZETJY+QYjehg38WMqCdoxoETjpMgdfhAxcq8XPKo2WYvYrWKpprPrM0hJ1AxIn4VVz+mYj/LSGBEyWPsNJrQzp7Dh1GP04K9JxwnQejr56HoIMSdAz1HWp1GxFQx38hCzQodYFSMiF8lJzUiwRlD+ULe2UYzRhQ/xs+eOFrb9/Bh9BP0jT1IclIjS3PKGdi7Cb6bZu4PfhzsjlMfL+IvrS8wpxg4lGcu2igBQ8WI+JXDbmNSaheAioJkF025sWQimZ4EWtj28Z5jMo4D26wLKTVnGEfXIiqDDsPg7EFWJxI5JiLq2MJ5m/9nbRapRMWI+N3Qrgm8ektv4p3HmmLyaMT/xTxJQWx7Yo7kwTvDYc9GC1NKjWyYZ3YOdETD0KetTiNyoo7Dze0mFSOBRJOeiSWGdk1gcJd40rMOsKegiLj6MSQnNcJxZCD8+yrIWwfvXA6j50J8V6vjSnWUHIbP/mzu939A075LYDp7sLlydN46c7RXg1ZWJxJ0Z0Qs5LDbSGnXmKt6tiClXWMcdhvUawJj5kFCDzi8D969AnZnWB1VqmPZC+DaAc5EuOBBq9OInFzdRtAqxdzflGZtFqmgYkQCT91GMPoTaNEXjvwC/74Sdq6yOpWcyp6NsOxFc3/IUxBV19I4IqfUYai53bTA2hxSQcWIBKY6DczF9VqlQJHLbLrJ/tbqVHIyHg988n/gKTX/ke98pdWJRE6tvN/I9mWajTVAqBiRwBUTCzfPgjYDoKTAXGQv62urU8mvrXwTdqab689c/jzYbKc/R8RKTdpD47PNAnrbIqvTCCpGJNBFnwUjP4S2F0NpIUy/AbZ9ZXUqKefaeWz9mUF/0aq8Ejw6ljfVaFRNIFAxIoEvqi7cNAPOHgJlR+D9EfoHJBAYBnz6IJQcgpbJ0Pc2qxOJVF95U83mNHCXWptFVIxIkIiMgRHvQacrwF0MM26GH2ZanSq8rX7XXI7dEQVXvgx2/XMiQSSxH9RravZJ267mX6vpXw8JHhFRcMO70P03YLjh4zvNlWHF//Zvg7Sjc4pcOhHiOlmbR8Rbdgd0uhyA3O9mMTdjFyu27cftMSwOFp406ZkEF0cEXP0qxDgh/V+w4CE4chAufEgdJ/3FXQYf32324WkzAM4bZ3UikRpZWac/fXkH+6b5/O7HYRjYSXDGMCm1C0O7JlgdL6zozogEH7sdhv0VBk4wv/7qSfjsEa3C6S/L/26OnomOhatfUfOMBKW0dTmM/CKafKMOcbaD9LJtBSDXVcQ9760mbV2OxQnDi/4VkeBks8HAh2HoM+bX306FT+4z/2oX3/l5BXw1xdwf9qym0pag5PYYTJ63nhIiWOTpDcBQx/cAlDfSTJ63Xk02fqRiRILbeffA1dPA5oCM6TBjJBQfsjpVaCrcB7PGmv11ut0IPX5jdSKRGknPOkCOqwiAz9znAjDE/j3lpYgB5LiKSM86YFHC8KNiRIJfz5vMkTYRMbDlM3PF34I8q1OFFo8HZt8BBTnQpANc8Xf10ZGgtaegqGJ/iac7RUYkre176GzLrvI48a0aFSNTp06lTZs2xMTE0K9fP9LT0095/IsvvkjHjh2pU6cOiYmJPPjggxQV6T+y1KJOw2HMp1C3MeT8AG8Mgr2brE4VOr5+DrZ9CRF1zBFN0WdZnUikxuLqx1TsHyGGpZ7uwLGmmpMdJ77ldTEyc+ZMxo8fz6RJk1i9ejU9evRgyJAh7Nmz56THv//++zz88MNMmjSJDRs28OabbzJz5kz+/Oc/n3F4kUoSz4Xbv4BG7cCVDW8ONteekDOzcQF89bS5f/nz0KyLtXlEzlByUiMSnDGU39tLO9pUM9Ru/mFtAxKcMSQnNbImYBjyuhh54YUXuOOOOxg7dixdunRh2rRp1K1bl7feeuukx3/zzTf079+fkSNH0qZNGy677DJuuumm095NEamRRm3htoXmhEZFLvjPNbB2ltWpglfeT2bzDIY5w2qvm61OJHLGHHYbk1LNotoGfOHpQ4nhoKN9Jx1sOwGYlNoFh11Nkf7iVTFSUlLCqlWrGDRo0LEfYLczaNAgVqxYcdJzzj//fFatWlVRfGRmZrJgwQKGDx9e5esUFxeTn59f6SFSbfUaw+i55uqx7hL4723mCBAN/fVO4T744DfmdO9JF5rDqUVCxNCuCbx6S2/inTHkU6+iqebGuit59ZbemmfEz7ya9Gzfvn243W6aNWtW6flmzZqxcePGk54zcuRI9u3bxwUXXIBhGJSVlXH33XefsplmypQpTJ482ZtoIpVFHu3bsPAxWPFPWPIM5P4I1/zLXA1YTq30iDnl/sFsaJhkXktHpNWpRGrV0K4JDO4ST3rWAaJ+GgGr13Bbwx+wnRNvdbSw4/PRNIsXL+bpp5/mlVdeYfXq1cyePZv58+fzxBNPVHnOhAkTcLlcFY8dO3b4OqaEIrsdhjwFV70CjmjYtADeuBT2bQHMuQZWbNuvaaB/zV0GH42FHd9CtBNGzoS6ajuX0OSw20hp15g+l40ERxS2fZtgzwarY4Udr+6MNGnSBIfDQV5e5WGTeXl5xMefvJJ87LHHGDVqFLfffjsA3bp1o7CwkDvvvJNHHnkE+0lmb4yOjiY6OtqbaCJV63WzuXbKjFtg32Z4/RJW9fkr961qVjHXAKBpoMFciXfe/bD5f+ZQ6ZEzoGlHq1OJ+F6ME9oPMv9o+Wm2Omr7mVd3RqKioujTpw+LFi2qeM7j8bBo0SJSUlJOes7hw4dPKDgcDgcAhqG/RMVPWvSBu5ZAqxQozqfX8nu57tAH2DjWjyQcp4GudHdo6z48nz9mTh5nc8D1b0Pr862OKOI/51xrbn/62CzMxW+8Xihv/PjxjBkzhr59+5KcnMyLL75IYWEhY8eOBWD06NG0aNGCKVPMKaNTU1N54YUX6NWrF/369WPr1q089thjpKamVhQlIn5xVhzuUXOZM2U013nSeCjyI3rZt/JQ6V38QiwGZs/6yfPWM7hLfMj3pE9bl8PkeeuP3h0ymBDxPikR881vXvkPc+4WkXDScajZpLt/K+Stg/huVicKG14XIyNGjGDv3r1MnDiR3NxcevbsSVpaWkWn1uzs7Ep3Qh599FFsNhuPPvoou3btomnTpqSmpvLUU0/V3m8hUk3p2Yf4/eHRfOdI5ImId7jUsYYF9j/zQMk40o3OlaaBTmnX2Oq4PpO2Lod73ltdMfn1YxHvcVvE/wCYVDqGlMhLGWplQBErRNeHswfDxk/NuyMqRvzGZgRBW0l+fj5OpxOXy0VsrEZCSM3NzdjFAzMyAOhs+5l/Rv6DdvYc3IaNqe6r+EfZtZQRwUu/6clVPVtYG9ZH3B6DC/76JTmuIux4mBTxLmMiFgLwSOmtvO8eRLwzhmV/uiTk7w6JnGDtLHM6gIZt4P4MLXtwhqr7+a21aSSsHD+98wajNaklTzHLfSEOm8H9EXOYHTWJdrZdIT0NdPkiYdGUMDXyJcZELMRj2PhT6R1Mdw/SImES3joOg8h68Mt22Pn9aQ+X2qFiRMLKr6eBPkwMD5XezbiS+zlo1KO7PYsF0Y/QL3e6OcQ1BO0pKKIR+XwQ9STDHN9TbETwQOk4ZrovPuE4kbATVQ86X2Hu//ihtVnCiIoRCSu/nga63HzPeQwpfpYl7u5EU4J94WPw5iDIXWtNUB9qU5rJx1ET6W3fykGjHqNKJjDPc+KomVC+OyRySt1uNLc/zQZ3qbVZwoSKEQk7x08DfTy7M4EjN86EK182J/vavQZeGwifPwrFBdaErW2r/033tOtobd9Dtqcp15X8hXSjc6VDtEiYhL22A6FeUzi831ytWnxOHVglbLk9BulZB9hTUERcffPDt6LDZkEuLPgDbPjE/PqseBj8OHS/MTg7tBW54H8Pww/vA7An/iIu234zLs7i+H8Ayn8zrc0hYe9/f4LvpkHX6+H6N61OE7Sq+/mtYkTkVDZ/BmkPw4FM8+sWfWDQX8yF44LF1i/gk/shfxfY7HDxI3DBeNLW5x03z4hJs9CKHLVzFbxxCUTUgT9sMYf9itdUjIjUlrJic7G9pc9DaaH5XLtL4ZJHzOIkUB3aA19Mhoz3zK8btjHX6WnTv+KQU94dEglnhgEv94ED28wFNnv8xupEQUnFiEhtO7QHljwLq94Gz9GRNm0vhgG/hzYXBE7zTWkRfPsKfP0ClBzt69Lvbrh0ojlSQESqZ/EzsHgKtLsERn1sdZqgpGJExFcOZJpFyY8fguE2n0voAefeAV2vg6i61foxtX5XoqQQVv8HvnkZ8neazzXvBUOfgVbn1fznioSrA5nwj16ADR78CZyhORGiL6kYEfG1X36Gb/5hFgDuYvO5GCd0u8F8tEyGk6xKDb9eF8ZU4/4av2yHNdPh+zfgyNGJyuo3h0GTzCGKVWQQkWp4axhkfwOXPAYXPmR1mqCjYkTEXwr3m/0yvn8TDv587HlnK+h0ubkseZv+EFkH+PW6MMd4NZIlfzdsWWjenfl52bHnG7aB/g9Aj5EQqXlCRM7Ymukw915o1Bb+b3XgNMcGCRUjIv7m8UDmV+baFhvmHeuvARARAy364mnem0e/j+K7wmbsNJpSTFSlH2GDE9eFcZfCvs2Q8yPk/ABZS2DP+spntR0IvUdD5yvB4fX6lyJSleJD8FwHs/P62P9B6xMnCJSqqRgRsVLpEXNI7ZaF5jZ/1wmHeAwbuTRkr9GAAqMOBdTFg41I3JybeBYNyTfnOzmUB4bnV2fbzJE8HYeZvfydLf3ze4mEoznjzLufPW+Bq6danSaoqBgRCRSGYd7Z2Pk9WT8spSAznSRbLvVtR6r/M6Lqm8uZx3eDxGSzd39dzZAq4hc/fwNvH11A76HNEH2W1YmCRnU/v3U/V8TXbDZo2hGadiQ3dhg3bfwWMGhEAa1teTSwHSKWQurbjmDDoJQIbr+oA+1bJUJsAtRPgHpx6ogqYpVWKWafkQOZsH4u9LrZ6kQhR8WIiB+Vrxqc6yriALEcMGI5vidreZ+RpwZdApp8TCQw2GzQ82b48glY856KER/Qn1oiflTVqsHHfz0ptYtmQRUJND1HmsspZH8DezdZnSbkqBgR8bOqVg2Od8ZogTqRQBXbHDoMM/dXvm1tlhCkDqwiFtG6MCJBZssXMP06c3LD32+qmDtIqqYOrCIBzmG3kdKusdUxRKS62l0CDVrBwWz46WOz6UZqhZppREREqsNuhz6/NfdXvmVplFCjYkRERKS6eo0CewTs/B5y11qdJmSoGBEREamus+Kg0xXmvjqy1hoVIyIiIt7oe6u5/XEmFLmszRIiVIyIiIh4I+lCaNoJSg6Zq/rKGVMxIiIi4g2bDfrdZe6n/ws8bmvzhAAVIyIiIt7qPsKcb+SX7bD5M6vTBD0VIyIiIt6Kqge9x5j7371qbZYQoGJERESkJpLvMNeryVoKeT9ZnSaoqRgRERGpiQatjg3z/e5f1mYJcipGREREauq8e8ztjzPh0F5rswSxGhUjU6dOpU2bNsTExNCvXz/S09NPefzBgwcZN24cCQkJREdH06FDBxYsWFCjwCIiIgGjVQo07w1lRfDdNKvTBC2vi5GZM2cyfvx4Jk2axOrVq+nRowdDhgxhz549Jz2+pKSEwYMHs337dmbNmsWmTZt4/fXXadGixRmHFxERsZTNBhc8aO5//zoUF1ibJ0jZDMMwvDmhX79+nHvuufzzn/8EwOPxkJiYyP/93//x8MMPn3D8tGnT+Nvf/sbGjRuJjIysUcjqLkEsIiLidx43TE2G/Vvhsqfg/PusThQwqvv57dWdkZKSElatWsWgQYOO/QC7nUGDBrFixYqTnvPJJ5+QkpLCuHHjaNasGV27duXpp5/G7dYkMSIiEgLsDjj/fnN/xVQoK7E2TxDyqhjZt28fbrebZs2aVXq+WbNm5ObmnvSczMxMZs2ahdvtZsGCBTz22GM8//zzPPnkk1W+TnFxMfn5+ZUeIiIiAavHb+CseCjYDWs/sjpN0PH5aBqPx0NcXByvvfYaffr0YcSIETzyyCNMm1Z1R58pU6bgdDorHomJib6OKSIiUnMR0cdG1ix/SVPEe8mrYqRJkyY4HA7y8vIqPZ+Xl0d8fPxJz0lISKBDhw44HI6K5zp37kxubi4lJSe/lTVhwgRcLlfFY8eOHd7EFBER8b++Y80p4vdtgvVzrE4TVLwqRqKioujTpw+LFi2qeM7j8bBo0SJSUlJOek7//v3ZunUrHo+n4rnNmzeTkJBAVFTUSc+Jjo4mNja20kNERCSgxTjhvHHm/pJndXfEC14304wfP57XX3+dd999lw0bNnDPPfdQWFjI2LFjARg9ejQTJkyoOP6ee+7hwIEDPPDAA2zevJn58+fz9NNPM27cuNr7LURERAJBv7vMomTvRt0d8UKEtyeMGDGCvXv3MnHiRHJzc+nZsydpaWkVnVqzs7Ox24/VOImJiXz22Wc8+OCDdO/enRYtWvDAAw/wpz/9qfZ+CxERkUBQp4F5d2Tx0+bdkS5Xm6Nt5JS8nmfECppnREREgkaRC17sZm6vfwu6Xmd1Isv4ZJ4REREROY0YJ6Qcnfhs8V/Vd6QaVIyIiIjUtn53QUwDc2TNDx9YnSbgqRgRERGpbTFOuPAhc/+rp6H0iLV5ApyKEREREV849w5wJkL+LvjuX1anCWgqRkRERHwhMgYufsTcX/YCHD5gbZ4ApmJERETEV7rfCHHnmCNrlv3d6jQBS8WIiIiIr9gdMHiyuf/dv+BAprV5ApSKEREREV9qPwjaXgzuYvjsUavTBCQVIyIiIr5ks8Gwv4I9AjbNh61fWJ0o4KgYERER8bWmHSH5LnP/fw9D2clXrQ9XKkZERET8YeCfoF5T2L8FvptmdZqAomJERETEH2KccOkkc3/JX8G109o8AUTFiIiIiL/0vBlaJkPJIZj/ewj8tWr9QsWIiIiIv9jtcOU/wB4Jm9PwrPuYFdv2MzdjFyu27cftCc/iJMLqACIiImElrjMM+D0seYaD//0d9xQ9y0HqA5DgjGFSaheGdk2wOKR/6c6IiIiIn33WaCSbPS1ohItHI6dXPJ/rKuKe91aTti7HwnT+p2JERETEj9weg78s2MrDpXfgMWxc71jKYPtKAMobaSbPWx9WTTYqRkRERPwoPesAOa4iVhsdeN09HIBnIl+nKb8AZkGS4yoiPSt8FtZTMSIiIuJHewqKKvafL7uRnzytaWwr4LnIf2HDc9LjQp2KERERET+Kqx9TsV9CJPeX3keREclFjh8Z4/j8pMeFOhUjIiIifpSc1IgEZwy2o19vM1rwVNnNAEyIeJ/utkwSnDEkJzWyLqSfqRgRERHxI4fdxqTULgAVBcl/3INZ6O5DtK2MV6P+zpOXxeOw26r+ISFGxYiIiIifDe2awKu39CbeWd4UY2N86T1k2xJoYdvPpesmgLvM0oz+ZDOMwJ+LNj8/H6fTicvlIjY21uo4IiIitcLtMUjPOsCegiLi6seQXC8Xx5uDobQQ+j8Agx+3OuIZqe7nt2ZgFRERsYjDbiOlXePjnmkMV/0TZo2F5S9B47Oh9yjL8vmLmmlEREQCSddrYcBD5v68B2Dz56c+PgSoGBEREQk0lzwKPW4Cww0fjYFdq6xO5FMqRkRERAKNzQZXvgztLoXSwzD9Bshbb3Uqn1ExIiIiEogckXDju5DQEw7vh3evgNy1VqfyCRUjIiIigSq6PoyeA817HS1IUmF3htWpap2KERERkUBWpyGMmgMt+sKRX+DfV0LmEqtT1SoVIyIiIoGuTgMY9TEkngdFLnjvWlj1rtWpak2NipGpU6fSpk0bYmJi6NevH+np6dU6b8aMGdhsNq6++uqavKyIiEj4iomF0XOh6/XgKYN598Nnj4C71OpkZ8zrYmTmzJmMHz+eSZMmsXr1anr06MGQIUPYs2fPKc/bvn07Dz30EAMGDKhxWBERkbAWGQPXvQEDJ5hfr/gnvDUUDmRam+sMeV2MvPDCC9xxxx2MHTuWLl26MG3aNOrWrctbb71V5Tlut5ubb76ZyZMn07Zt2zMKLCIiEtZsNhj4MNzwDkQ7YddKmDYA1kyHwF/h5aS8KkZKSkpYtWoVgwYNOvYD7HYGDRrEihUrqjzv8ccfJy4ujttuu61ar1NcXEx+fn6lh4iIiBznnGvgnuXQuj+UHIK598I7V0DeT1Yn85pXxci+fftwu900a9as0vPNmjUjNzf3pOcsW7aMN998k9dff73arzNlyhScTmfFIzEx0ZuYIiIi4aFBIoyZB4P+AhF14Odl5l2SBX+AgpN/Lgcin46mKSgoYNSoUbz++us0adKk2udNmDABl8tV8dixY4cPU4qIiAQxuwMueBDuS4fOV5pTyKe/Bi92N4sS184qT3V7DFZs28/cjF2s2LYft8eaZh6vVu1t0qQJDoeDvLy8Ss/n5eURHx9/wvHbtm1j+/btpKamVjzn8XjMF46IYNOmTbRr1+6E86Kjo4mOjvYmmoiISHhr0ApG/AcyF8OXT8HOdLMoWfkWdBgKfX4L7S4xixcgbV0Ok+etJ8dVVPEjEpwxTErtwtCuCX6NbjMM73q79OvXj+TkZF5++WXALC5atWrFfffdx8MPP1zp2KKiIrZu3VrpuUcffZSCggJeeuklOnToQFRU1GlfMz8/H6fTicvlIjY21pu4IiIi4ccwIGsJLPmb2XRTLrYl9LqFxfUuY+zsXH5dANiObl+9pXetFCTV/fz26s4IwPjx4xkzZgx9+/YlOTmZF198kcLCQsaOHQvA6NGjadGiBVOmTCEmJoauXbtWOr9BgwYAJzwvIiIitcRmg7YDzUfeT7D6P/DDB5C/E5Y8w4X8lbcjuzPTPZBFnt6UEAmAgVmQTJ63nsFd4nHYbad4kdrjdTEyYsQI9u7dy8SJE8nNzaVnz56kpaVVdGrNzs7GbtfEriIiIgGh2Tkw7Bmzk+vGT3EtfwNn7goGOn5goOMHDhhnMcd9AdPdl7LNaIEB5LiKSM86QEq7xn6J6HUzjRXUTCMiIlI75mbs4oWZadzgWML1jqXE234B4NHSsbznHlxx3Eu/6clVPVuc0Wv5rJlGREREgldc/Rh+NuJ5rmwEL5TdwIX2H7nOsZRP3OefcJy/qBgREREJI8lJjUhwxpDrKsKDncWeniz29Kz4vg2Id8aQnNTIb5nUuUNERCSMOOw2JqV2AY6NnilX/vWk1C5+67wKKkZERETCztCuCbx6S2/inZWbYuKdMbU2rNcbaqYREREJQ0O7JjC4SzzpWQfYU1BEXH2zacafd0TKqRgREREJUw67zW/Dd09FzTQiIiJiKRUjIiIiYikVIyIiImIpFSMiIiJiKRUjIiIiYikVIyIiImIpFSMiIiJiKRUjIiIiYikVIyIiImKpoJiB1TAMAPLz8y1OIiIiItVV/rld/jlelaAoRgoKCgBITEy0OImIiIh4q6CgAKfTWeX3bcbpypUA4PF42L17N/Xr18dmq70FfPLz80lMTGTHjh3ExsbW2s+VE+la+4eus3/oOvuHrrN/+PI6G4ZBQUEBzZs3x26vumdIUNwZsdvttGzZ0mc/PzY2Vv+j+4mutX/oOvuHrrN/6Dr7h6+u86nuiJRTB1YRERGxlIoRERERsVRYFyPR0dFMmjSJ6Ohoq6OEPF1r/9B19g9dZ//QdfaPQLjOQdGBVUREREJXWN8ZEREREeupGBERERFLqRgRERERS6kYEREREUuFfDEydepU2rRpQ0xMDP369SM9Pf2Ux3/00Ud06tSJmJgYunXrxoIFC/yUNPh5c61ff/11BgwYQMOGDWnYsCGDBg067X8bMXn7/3S5GTNmYLPZuPrqq30bMER4e50PHjzIuHHjSEhIIDo6mg4dOujfj2rw9jq/+OKLdOzYkTp16pCYmMiDDz5IUVGRn9IGp6VLl5Kamkrz5s2x2WzMmTPntOcsXryY3r17Ex0dTfv27XnnnXd8G9IIYTNmzDCioqKMt956y/jpp5+MO+64w2jQoIGRl5d30uOXL19uOBwO49lnnzXWr19vPProo0ZkZKSxdu1aPycPPt5e65EjRxpTp0411qxZY2zYsMH47W9/azidTmPnzp1+Th5cvL3O5bKysowWLVoYAwYMMK666ir/hA1i3l7n4uJio2/fvsbw4cONZcuWGVlZWcbixYuNjIwMPycPLt5e5+nTpxvR0dHG9OnTjaysLOOzzz4zEhISjAcffNDPyYPLggULjEceecSYPXu2ARgff/zxKY/PzMw06tata4wfP95Yv3698fLLLxsOh8NIS0vzWcaQLkaSk5ONcePGVXztdruN5s2bG1OmTDnp8TfeeKNx+eWXV3quX79+xl133eXTnKHA22v9a2VlZUb9+vWNd99911cRQ0JNrnNZWZlx/vnnG2+88YYxZswYFSPV4O11fvXVV422bdsaJSUl/ooYEry9zuPGjTMuueSSSs+NHz/e6N+/v09zhpLqFCN//OMfjXPOOafScyNGjDCGDBnis1wh20xTUlLCqlWrGDRoUMVzdrudQYMGsWLFipOes2LFikrHAwwZMqTK48VUk2v9a4cPH6a0tJRGjRr5KmbQq+l1fvzxx4mLi+O2227zR8ygV5Pr/Mknn5CSksK4ceNo1qwZXbt25emnn8btdvsrdtCpyXU+//zzWbVqVUVTTmZmJgsWLGD48OF+yRwurPgsDIqF8mpi3759uN1umjVrVun5Zs2asXHjxpOek5ube9Ljc3NzfZYzFNTkWv/an/70J5o3b37CG0COqcl1XrZsGW+++SYZGRl+SBgaanKdMzMz+fLLL7n55ptZsGABW7du5d5776W0tJRJkyb5I3bQqcl1HjlyJPv27eOCCy7AMAzKysq4++67+fOf/+yPyGGjqs/C/Px8jhw5Qp06dWr9NUP2zogEj2eeeYYZM2bw8ccfExMTY3WckFFQUMCoUaN4/fXXadKkidVxQprH4yEuLo7XXnuNPn36MGLECB555BGmTZtmdbSQsnjxYp5++mleeeUVVq9ezezZs5k/fz5PPPGE1dHkDIXsnZEmTZrgcDjIy8ur9HxeXh7x8fEnPSc+Pt6r48VUk2td7rnnnuOZZ57hiy++oHv37r6MGfS8vc7btm1j+/btpKamVjzn8XgAiIiIYNOmTbRr1863oYNQTf5/TkhIIDIyEofDUfFc586dyc3NpaSkhKioKJ9mDkY1uc6PPfYYo0aN4vbbbwegW7duFBYWcuedd/LII49gt+vv69pQ1WdhbGysT+6KQAjfGYmKiqJPnz4sWrSo4jmPx8OiRYtISUk56TkpKSmVjgdYuHBhlceLqSbXGuDZZ5/liSeeIC0tjb59+/ojalDz9jp36tSJtWvXkpGRUfG48sorufjii8nIyCAxMdGf8YNGTf5/7t+/P1u3bq0o9gA2b95MQkKCCpEq1OQ6Hz58+ISCo7wANLTMWq2x5LPQZ11jA8CMGTOM6Oho45133jHWr19v3HnnnUaDBg2M3NxcwzAMY9SoUcbDDz9ccfzy5cuNiIgI47nnnjM2bNhgTJo0SUN7q8nba/3MM88YUVFRxqxZs4ycnJyKR0FBgVW/QlDw9jr/mkbTVI+31zk7O9uoX7++cd999xmbNm0yPv30UyMuLs548sknrfoVgoK313nSpElG/fr1jQ8++MDIzMw0Pv/8c6Ndu3bGjTfeaNWvEBQKCgqMNWvWGGvWrDEA44UXXjDWrFlj/Pzzz4ZhGMbDDz9sjBo1quL48qG9f/jDH4wNGzYYU6dO1dDeM/Xyyy8brVq1MqKioozk5GTj22+/rfjeRRddZIwZM6bS8R9++KHRoUMHIyoqyjjnnHOM+fPn+zlx8PLmWrdu3doATnhMmjTJ/8GDjLf/Tx9PxUj1eXudv/nmG6Nfv35GdHS00bZtW+Opp54yysrK/Jw6+HhznUtLS42//OUvRrt27YyYmBgjMTHRuPfee41ffvnF/8GDyFdffXXSf2/Lr+2YMWOMiy666IRzevbsaURFRRlt27Y13n77bZ9mtBmG7m2JiIiIdUK2z4iIiIgEBxUjIiIiYikVIyIiImIpFSMiIiJiKRUjIiIiYikVIyIiImIpFSMiIiJiKRUjIiIiYikVIyIiImIpFSMiIiJiKRUjIiIiYikVIyIiImKp/wcywQpPPD/YrAAAAABJRU5ErkJggg==" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x = np.array([k / 14.0 for k in range(15)])\n", + "f = np.exp(x * np.cos(3.0 * x * np.pi))\n", + "\n", + "rbf = Rbf(x, f, function='gaussian', epsilon=1/3.0)\n", + "\n", + "x_out = np.array([k / 149.0 for k in range(150)])\n", + "f_interp = rbf(x_out)\n", + "\n", + "plt.plot(x, f, 'o')\n", + "plt.plot(x_out, f_interp , '-')\n", + "\n", + "f_interp" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-09-28T10:02:18.620636465Z", + "start_time": "2023-09-28T10:02:18.488559928Z" + } + }, + "id": "e57f3bfc5a6cb204" + }, + { + "cell_type": "code", + "execution_count": 15, + "outputs": [ + { + "data": { + "text/plain": "array([0.69464463, 0.69308183, 0.69150736, 0.68992139, 0.68832406,\n 0.68671551, 0.68509589, 0.68346531, 0.68182389, 0.68017171,\n 0.67850888, 0.67683548, 0.67515156, 0.6734572 , 0.67175245,\n 0.67003733, 0.66831189, 0.66657615, 0.66483011, 0.66307377,\n 0.66130712, 0.65953014, 0.65774281, 0.65594508, 0.6541369 ,\n 0.65231821, 0.65048893, 0.64864899, 0.64679829, 0.64493672,\n 0.64306417, 0.64118051, 0.63928561, 0.63737931, 0.63546146,\n 0.63353187, 0.63159038, 0.62963677, 0.62767084, 0.62569237,\n 0.62370112, 0.62169685, 0.61967931, 0.61764821, 0.61560328,\n 0.61354422, 0.61147072, 0.60938246, 0.60727911, 0.60516031,\n 0.60302571, 0.60087495, 0.59870763, 0.59652337, 0.59432175,\n 0.59210238, 0.58986482, 0.58760865, 0.58533341, 0.58533341,\n 0.58303867, 0.58072398, 0.57838887, 0.57603288, 0.57365555,\n 0.57125641, 0.568835 , 0.56639087, 0.56392355, 0.5614326 ,\n 0.55891758, 0.55637805, 0.55381361, 0.55122386, 0.54860842,\n 0.54596693, 0.54329907, 0.54060452, 0.53788302, 0.53513433,\n 0.53235824, 0.5295546 , 0.52672327, 0.52386419, 0.52097732,\n 0.51806269, 0.51512038, 0.5121505 , 0.50915325, 0.50612887,\n 0.50307767, 0.5 , 0.4968963 , 0.49376705, 0.4906128 ,\n 0.48743418, 0.48423185, 0.48100655, 0.47775909, 0.47449034,\n 0.4712012 , 0.46789267, 0.46456578, 0.46122162, 0.45786134,\n 0.45448613, 0.45109726, 0.44769602, 0.44428374, 0.44086183,\n 0.43743171, 0.43399486, 0.4305528 , 0.42710707, 0.42365927,\n 0.42021102, 0.416764 , 0.41331988, 0.4098804 , 0.40644733,\n 0.40302245, 0.3996076 , 0.39620462, 0.3928154 , 0.38944187,\n 0.38608597, 0.38274969, 0.37943505, 0.37614408, 0.37287886,\n 0.36964152, 0.3664342 , 0.36325908, 0.36011837, 0.35701434,\n 0.35394927, 0.35092549, 0.34794536, 0.34501129, 0.34212571,\n 0.33929111, 0.33650999, 0.33378492, 0.33111848, 0.32851331,\n 0.32597206, 0.32349743, 0.32109215, 0.31875898, 0.31650072,\n 0.31432016, 0.31222016, 0.31020357, 0.30827325, 0.30643209,\n 0.30468296, 0.30302876, 0.30147235, 0.30001661, 0.29866436,\n 0.29741843, 0.2962816 , 0.29525658, 0.29434606, 0.29355265,\n 0.29287889, 0.29232723, 0.29190003, 0.29159955, 0.29142793,\n 0.29138718, 0.2914792 , 0.29170571, 0.29206829, 0.29256837,\n 0.29320718, 0.29398581, 0.29490512, 0.29596581, 0.29716836,\n 0.29851306, 0.3 , 0.30162905, 0.30339988, 0.30531196,\n 0.30736453, 0.30955665, 0.31188717, 0.31435474, 0.31695784,\n 0.31969475, 0.32256357, 0.32556225, 0.32868857, 0.33194015,\n 0.33531448, 0.33880892, 0.34242071, 0.34614696, 0.34998469,\n 0.35393082, 0.35798222, 0.36213562, 0.36638776, 0.37073525,\n 0.37517472, 0.3797027 , 0.38431572, 0.38901027, 0.39378283,\n 0.39862985, 0.40354777, 0.40853303, 0.41358206, 0.4186913 ,\n 0.42385719, 0.42907617, 0.43434469, 0.43965922, 0.44501624,\n 0.45041222, 0.45584367, 0.4613071 , 0.46679904, 0.47231604,\n 0.47785464, 0.48341142, 0.48898296, 0.49456585, 0.5001567 ,\n 0.50575214, 0.51134878, 0.51694328, 0.52253228, 0.52811245,\n 0.53368045, 0.53923298, 0.54476672, 0.55027838, 0.55576468,\n 0.56122234, 0.56664811, 0.57203875, 0.57739104, 0.58270178,\n 0.58796779, 0.59318592, 0.59835305, 0.60346609, 0.60852201,\n 0.61351779, 0.61845048, 0.62331718, 0.62811505, 0.63284131,\n 0.63749327, 0.64206831, 0.64656388, 0.65097754, 0.65530696,\n 0.65954989, 0.66370421, 0.66776792, 0.67173914, 0.67561613,\n 0.67939727, 0.68308111, 0.68666633, 0.69015176, 0.6935364 ,\n 0.69681937, 0.7 , 0.70307774, 0.7060522 , 0.70892317,\n 0.71169059, 0.71435453, 0.71691524, 0.7193731 , 0.72172864,\n 0.7239825 , 0.72613549, 0.72818851, 0.73014259, 0.73199887,\n 0.73375858, 0.73542305, 0.7369937 , 0.73847202, 0.73985957,\n 0.74115796, 0.74236887, 0.74349402, 0.74453517, 0.7454941 ,\n 0.74637263, 0.74717258, 0.7478958 , 0.74854413, 0.74911943,\n 0.74962353, 0.75005827, 0.75042547, 0.75072693, 0.75096445,\n 0.75113978, 0.75125466, 0.75131079, 0.75130986, 0.75125351,\n 0.75114335, 0.75098096, 0.75076789, 0.75050563, 0.75019565,\n 0.74983939, 0.74943824, 0.74899356, 0.74850665, 0.74797881,\n 0.74741128, 0.74680526, 0.74616191, 0.74548238, 0.74476776,\n 0.74401911, 0.74323746, 0.74242382, 0.74157913, 0.74070433,\n 0.73980031, 0.73886796, 0.7379081 , 0.73692155, 0.73590908,\n 0.73487145, 0.73380939, 0.7327236 , 0.73161476, 0.73048351,\n 0.72933049, 0.7281563 , 0.72696153, 0.72574673, 0.72451244,\n 0.7232592 , 0.72198749, 0.72069779, 0.71939058, 0.71806629,\n 0.71672535, 0.71536817, 0.71399514, 0.71260665, 0.71120305,\n 0.70978469, 0.7083519 , 0.706905 , 0.7054443 , 0.70397008,\n 0.70248262, 0.70098218, 0.69946903, 0.69794338, 0.69640548,\n 0.69485553])" + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": "
", + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x = np.array([90, 180, 270])\n", + "f = np.array([0.5, 0.3, 0.7])\n", + "\n", + "x = np.concatenate([x - 365, x, x + 365])\n", + "f = np.concatenate([f, f, f])\n", + "\n", + "rbf = Rbf(x, f, function='multiquadric', epsilon=50.0)\n", + "\n", + "x_out = np.array([k for k in range(365)])\n", + "f_interp = rbf(x_out)\n", + "\n", + "f_interp = np.concatenate([f_interp[:58], [f_interp[58]], f_interp[58:]])\n", + "assert len(f_interp) == 366\n", + "x_plot = np.array([k + 1 for k in range(366)])\n", + "\n", + "plt.plot(x, f, 'o')\n", + "plt.plot(x_plot-366, f_interp , '-')\n", + "plt.plot(x_plot, f_interp , '-')\n", + "plt.plot(x_plot+366, f_interp , '-')\n", + "\n", + "f_interp" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-09-28T10:35:02.510073929Z", + "start_time": "2023-09-28T10:35:02.352115352Z" + } + }, + "id": "a19ad6146a888df3" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pywr-core/src/parameters/profiles/rbf.rs b/pywr-core/src/parameters/profiles/rbf.rs index 190c9831..e92d935f 100644 --- a/pywr-core/src/parameters/profiles/rbf.rs +++ b/pywr-core/src/parameters/profiles/rbf.rs @@ -1,9 +1,13 @@ use crate::network::Network; -use crate::parameters::{downcast_internal_state, Parameter, ParameterMeta, VariableParameter}; +use crate::parameters::{ + downcast_internal_state_mut, downcast_internal_state_ref, downcast_variable_config_ref, Parameter, ParameterMeta, + VariableConfig, VariableParameter, +}; use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; use crate::timestep::Timestep; use crate::PywrError; +use chrono::Datelike; use nalgebra::DMatrix; use std::any::Any; @@ -29,21 +33,70 @@ pub struct RbfProfileParameter { meta: ParameterMeta, points: Vec<(u32, f64)>, function: RadialBasisFunction, - variable: Option, +} + +/// The internal state of the RbfProfileParameter. +/// +/// This holds the interpolated profile along with any points that have been updated via the optimisation API. +#[derive(Clone)] +struct RbfProfileInternalState { + /// The interpolated profile. + profile: [f64; 366], + /// Optional updated x values of the points. + points_x: Option>, + /// Optional updated y values of the points. + points_y: Option>, +} + +impl RbfProfileInternalState { + fn new(points: &[(u32, f64)], function: &RadialBasisFunction) -> Self { + let profile = interpolate_rbf_profile(points, function); + + Self { + profile, + points_x: None, + points_y: None, + } + } + + /// Update the x values of the points. + /// + /// This does not update the profile. + fn update_x(&mut self, x: Vec) { + self.points_x = Some(x); + } + + /// Update the y values of the points. + /// + /// This does not update the profile. + fn update_y(&mut self, y: Vec) { + self.points_y = Some(y); + } + + /// Update the profile with the given points used as default. Any locally stored x and y values are + /// used in preference to the default points when interpolating the profile. + fn update_profile(&mut self, points: &[(u32, f64)], function: &RadialBasisFunction) { + let points: Vec<_> = match (&self.points_x, &self.points_y) { + (Some(x), Some(y)) => x.iter().zip(y.iter()).map(|(x, y)| (*x, *y)).collect(), + (Some(x), None) => x + .iter() + .zip(points.iter().map(|(_, y)| *y)) + .map(|(x, y)| (*x, y)) + .collect(), + (None, Some(y)) => points.iter().zip(y.iter()).map(|((x, _), y)| (*x, *y)).collect(), + (None, None) => points.to_vec(), + }; + + self.profile = interpolate_rbf_profile(&points, function); + } } impl RbfProfileParameter { - pub fn new( - name: &str, - points: Vec<(u32, f64)>, - function: RadialBasisFunction, - variable: Option, - ) -> Self { + pub fn new(name: &str, points: Vec<(u32, f64)>, function: RadialBasisFunction) -> Self { Self { meta: ParameterMeta::new(name), points, function, - variable, } } } @@ -62,8 +115,8 @@ impl Parameter for RbfProfileParameter { _timesteps: &[Timestep], _scenario_index: &ScenarioIndex, ) -> Result>, PywrError> { - let profile = interpolate_rbf_profile(&self.points, &self.function); - Ok(Some(Box::new(profile))) + let internal_state = RbfProfileInternalState::new(&self.points, &self.function); + Ok(Some(Box::new(internal_state))) } fn compute( @@ -75,9 +128,9 @@ impl Parameter for RbfProfileParameter { internal_state: &mut Option>, ) -> Result { // Get the profile from the internal state - let profile = downcast_internal_state::<[f64; 366]>(internal_state); + let internal_state = downcast_internal_state_ref::(internal_state); // Return today's value from the profile - Ok(profile[timestep.date.ordinal() as usize - 1]) + Ok(internal_state.profile[timestep.date.ordinal() as usize - 1]) } fn as_f64_variable(&self) -> Option<&dyn VariableParameter> { @@ -98,21 +151,28 @@ impl Parameter for RbfProfileParameter { } impl VariableParameter for RbfProfileParameter { - fn is_active(&self) -> bool { - self.variable.is_some() + fn meta(&self) -> &ParameterMeta { + &self.meta } /// The size is the number of points that define the profile. - fn size(&self) -> usize { + fn size(&self, _variable_config: &dyn VariableConfig) -> usize { self.points.len() } /// The f64 values update the profile value of each point. - fn set_variables(&mut self, values: &[f64]) -> Result<(), PywrError> { + fn set_variables( + &self, + values: &[f64], + _variable_config: &dyn VariableConfig, + internal_state: &mut Option>, + ) -> Result<(), PywrError> { if values.len() == self.points.len() { - for (point, v) in self.points.iter_mut().zip(values) { - point.1 = *v; - } + let value = downcast_internal_state_mut::(internal_state); + + value.update_y(values.to_vec()); + value.update_profile(&self.points, &self.function); + Ok(()) } else { Err(PywrError::ParameterVariableValuesIncorrectLength) @@ -120,45 +180,50 @@ impl VariableParameter for RbfProfileParameter { } /// The f64 values are the profile values of each point. - fn get_variables(&self) -> Vec { - self.points.iter().map(|p| p.1).collect() + fn get_variables(&self, internal_state: &Option>) -> Option> { + let value = downcast_internal_state_ref::(internal_state); + value.points_y.clone() } - fn get_lower_bounds(&self) -> Result, PywrError> { - if let Some(variable) = &self.variable { - let lb = (0..self.points.len()).map(|_| variable.value_lower_bounds).collect(); - Ok(lb) - } else { - Err(PywrError::ParameterVariableNotActive) - } + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError> { + let config = downcast_variable_config_ref::(variable_config); + let lb = (0..self.points.len()).map(|_| config.value_lower_bounds).collect(); + Ok(lb) } - fn get_upper_bounds(&self) -> Result, PywrError> { - if let Some(variable) = &self.variable { - let ub = (0..self.points.len()).map(|_| variable.value_upper_bounds).collect(); - Ok(ub) - } else { - Err(PywrError::ParameterVariableNotActive) - } + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError> { + let config = downcast_variable_config_ref::(variable_config); + let lb = (0..self.points.len()).map(|_| config.value_upper_bounds).collect(); + Ok(lb) } } impl VariableParameter for RbfProfileParameter { - fn is_active(&self) -> bool { - self.variable.as_ref().is_some_and(|v| v.days_of_year_range.is_some()) + fn meta(&self) -> &ParameterMeta { + &self.meta } - /// The size is the number of points that define the profile. - fn size(&self) -> usize { - self.points.len() + fn size(&self, variable_config: &dyn VariableConfig) -> usize { + let config = downcast_variable_config_ref::(variable_config); + match config.days_of_year_range { + Some(_) => self.points.len(), + None => 0, + } } /// Sets the day of year for each point. - fn set_variables(&mut self, values: &[u32]) -> Result<(), PywrError> { + fn set_variables( + &self, + values: &[u32], + _variable_config: &dyn VariableConfig, + internal_state: &mut Option>, + ) -> Result<(), PywrError> { if values.len() == self.points.len() { - for (point, v) in self.points.iter_mut().zip(values) { - point.0 = *v; - } + let value = downcast_internal_state_mut::(internal_state); + + value.update_x(values.to_vec()); + value.update_profile(&self.points, &self.function); + Ok(()) } else { Err(PywrError::ParameterVariableValuesIncorrectLength) @@ -166,43 +231,40 @@ impl VariableParameter for RbfProfileParameter { } /// Returns the day of year for each point. - fn get_variables(&self) -> Vec { - self.points.iter().map(|p| p.0).collect() + fn get_variables(&self, internal_state: &Option>) -> Option> { + let value = downcast_internal_state_ref::(internal_state); + value.points_x.clone() } - fn get_lower_bounds(&self) -> Result, PywrError> { - if let Some(variable) = &self.variable { - if let Some(days_of_year_range) = &variable.days_of_year_range { - // Make sure the lower bound is not less than 1 and handle integer underflow - let lb = self - .points - .iter() - .map(|p| p.0.checked_sub(*days_of_year_range).unwrap_or(1).max(1)) - .collect(); - - Ok(lb) - } else { - Err(PywrError::ParameterVariableNotActive) - } + fn get_lower_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError> { + let config = downcast_variable_config_ref::(variable_config); + + if let Some(days_of_year_range) = &config.days_of_year_range { + // Make sure the lower bound is not less than 1 and handle integer underflow + let lb = self + .points + .iter() + .map(|p| p.0.checked_sub(*days_of_year_range).unwrap_or(1).max(1)) + .collect(); + + Ok(lb) } else { Err(PywrError::ParameterVariableNotActive) } } - fn get_upper_bounds(&self) -> Result, PywrError> { - if let Some(variable) = &self.variable { - if let Some(days_of_year_range) = &variable.days_of_year_range { - // Make sure the upper bound is not greater than 365 and handle integer overflow - let lb = self - .points - .iter() - .map(|p| p.0.checked_add(*days_of_year_range).unwrap_or(365).min(365)) - .collect(); - - Ok(lb) - } else { - Err(PywrError::ParameterVariableNotActive) - } + fn get_upper_bounds(&self, variable_config: &dyn VariableConfig) -> Result, PywrError> { + let config = downcast_variable_config_ref::(variable_config); + + if let Some(days_of_year_range) = &config.days_of_year_range { + // Make sure the upper bound is not greater than 365 and handle integer overflow + let lb = self + .points + .iter() + .map(|p| p.0.checked_add(*days_of_year_range).unwrap_or(365).min(365)) + .collect(); + + Ok(lb) } else { Err(PywrError::ParameterVariableNotActive) } diff --git a/pywr-core/src/parameters/profiles/uniform_drawdown.rs b/pywr-core/src/parameters/profiles/uniform_drawdown.rs index 0b55afaa..a34d4439 100644 --- a/pywr-core/src/parameters/profiles/uniform_drawdown.rs +++ b/pywr-core/src/parameters/profiles/uniform_drawdown.rs @@ -4,8 +4,8 @@ use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; use crate::timestep::Timestep; use crate::PywrError; +use chrono::{Datelike, NaiveDate}; use std::any::Any; -use time::{Date, Month}; fn is_leap_year(year: i32) -> bool { (year % 4 == 0) & ((year % 100 != 0) | (year % 400 == 0)) @@ -18,11 +18,11 @@ pub struct UniformDrawdownProfileParameter { } impl UniformDrawdownProfileParameter { - pub fn new(name: &str, reset_day: u8, reset_month: Month, residual_days: u8) -> Self { + pub fn new(name: &str, reset_day: u32, reset_month: u32, residual_days: u8) -> Self { // Calculate the reset day of year in a known leap year. - let reset_doy = Date::from_calendar_date(2016, reset_month, reset_day) + let reset_doy = NaiveDate::from_ymd_opt(2016, reset_month, reset_day) .expect("Invalid reset day") - .ordinal(); + .ordinal() as u16; Self { meta: ParameterMeta::new(name), diff --git a/pywr-core/src/parameters/profiles/weekly.rs b/pywr-core/src/parameters/profiles/weekly.rs new file mode 100644 index 00000000..4eadb5b4 --- /dev/null +++ b/pywr-core/src/parameters/profiles/weekly.rs @@ -0,0 +1,454 @@ +use crate::network::Network; +use crate::parameters::{Parameter, ParameterMeta}; +use crate::scenario::ScenarioIndex; +use crate::state::{ParameterState, State}; +use crate::timestep::Timestep; +use crate::PywrError; +use chrono::{Datelike, NaiveDate}; +use std::any::Any; +use thiserror::Error; + +pub enum WeeklyInterpDay { + First, + Last, +} + +// A weekly profile can be 52 or 53 week long +pub enum WeeklyProfileValues { + FiftyTwo([f64; 52]), + FiftyThree([f64; 53]), +} + +impl WeeklyProfileValues { + /// Get the week position in a calendar year from date. The position starts from 0 on the + /// first week day and ends with 1 on the last week day. + fn current_pos(&self, date: &NaiveDate) -> f64 { + let current_day = date.ordinal(); + (current_day - 1) as f64 / 7.0 + } + + /// Get the week index from the provided date + fn current_index(&self, date: &NaiveDate) -> usize { + let current_day = date.ordinal(); + let current_pos = self.current_pos(date) as usize; + + // if year is leap the last week starts on the 365th day + let is_leap_year = NaiveDate::from_ymd_opt(date.year(), 1, 1).unwrap().leap_year(); + let last_week_day_start = if is_leap_year { 365 } else { 364 }; + + match self { + Self::FiftyTwo(_) => { + if current_day >= last_week_day_start { + 51 + } else { + current_pos + } + } + Self::FiftyThree(_) => { + if current_day >= last_week_day_start { + 52 + } else { + current_pos + } + } + } + } + + /// Get the value corresponding to the week index for the provided date + fn current(&self, date: &NaiveDate) -> f64 { + // The current_index function always returns and index between 0 and + // 52 (for Self::FiftyTwo) or 53 (Self::FiftyThree). This ensures + // that the index is always in range in the value array below + let current_index = self.current_index(date); + + match self { + Self::FiftyTwo(values) => values[current_index], + Self::FiftyThree(values) => values[current_index], + } + } + + /// Get the next week's value based on the week index of the provided date. If the current + /// week is larger than the array length, the value corresponding to the first week is + /// returned. + fn next(&self, date: &NaiveDate) -> f64 { + let current_week_index = self.current_index(date); + + match self { + Self::FiftyTwo(values) => { + if current_week_index >= 51 { + values[0] + } else { + values[current_week_index + 1] + } + } + Self::FiftyThree(values) => { + if current_week_index >= 52 { + values[0] + } else { + values[current_week_index + 1] + } + } + } + } + + /// Get the previous week's value based on the week index of the provided date. If the + /// current week index is 0 than the last array value is returned. + fn prev(&self, date: &NaiveDate) -> f64 { + let current_week_index = self.current_index(date); + + match self { + Self::FiftyTwo(values) => { + if current_week_index == 0 { + values[51] + } else { + values[current_week_index - 1] + } + } + Self::FiftyThree(values) => { + if current_week_index == 0 { + values[52] + } else { + values[current_week_index - 1] + } + } + } + } + + /// Find the value corresponding to the given date by linearly interpolating between two + /// consecutive week's values. + fn interpolate(&self, date: &NaiveDate, first_value: f64, last_value: f64) -> f64 { + let current_pos = self.current_pos(date); + let week_delta = current_pos - current_pos.floor(); + first_value + (last_value - first_value) * week_delta + } + + /// Calculate the value on the given date using the interpolation method option. In a 52-week + /// interpolated profile, the upper boundary in the 52nd and 53rd week is the same when + /// WeeklyInterpDay is First (i.e. the value on 1st January). When WeeklyInterpDay is Last the + /// 1st and last week will share the same lower bound (i.e. the value on the last week). + fn value(&self, date: &NaiveDate, interp_day: &Option) -> f64 { + match interp_day { + None => self.current(date), + Some(interp_day) => match interp_day { + WeeklyInterpDay::First => { + let first_value = self.current(date); + let last_value = self.next(date); + self.interpolate(date, first_value, last_value) + } + WeeklyInterpDay::Last => { + let first_value = self.prev(date); + let last_value = self.current(date); + self.interpolate(date, first_value, last_value) + } + }, + } + } +} + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum WeeklyProfileError { + #[error("52 or 53 values must be given for a weekly profile parameter")] + InvalidLength, +} + +impl TryFrom<&[f64]> for WeeklyProfileValues { + type Error = WeeklyProfileError; + + fn try_from(value: &[f64]) -> Result { + match value.len() { + 52 => Ok(WeeklyProfileValues::FiftyTwo(value.try_into().unwrap())), + 53 => Ok(WeeklyProfileValues::FiftyThree(value.try_into().unwrap())), + _ => Err(WeeklyProfileError::InvalidLength), + } + } +} + +/// Weekly profile parameter. This supports a profile with either 52 or 53 weeks, with or without interpolation. +pub struct WeeklyProfileParameter { + meta: ParameterMeta, + values: WeeklyProfileValues, + interp_day: Option, +} + +impl WeeklyProfileParameter { + pub fn new(name: &str, values: WeeklyProfileValues, interp_day: Option) -> Self { + Self { + meta: ParameterMeta::new(name), + values, + interp_day, + } + } +} + +impl Parameter for WeeklyProfileParameter { + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn meta(&self) -> &ParameterMeta { + &self.meta + } + fn compute( + &self, + timestep: &Timestep, + _scenario_index: &ScenarioIndex, + _model: &Network, + _state: &State, + _internal_state: &mut Option>, + ) -> Result { + Ok(self.values.value(×tep.date.date(), &self.interp_day)) + } +} + +#[cfg(test)] +mod tests { + use crate::parameters::profiles::weekly::{WeeklyInterpDay, WeeklyProfileValues}; + use crate::test_utils::assert_approx_array_eq; + use chrono::{Datelike, NaiveDate, TimeDelta}; + + /// Build a time-series from the weekly profile + fn collect(week_size: &WeeklyProfileValues, interp_day: Option) -> Vec { + let dt0 = NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(); + let dt1 = NaiveDate::from_ymd_opt(2020, 12, 31).unwrap(); + + let mut dt = dt0; + let mut data: Vec = Vec::new(); + while dt <= dt1 { + let date = NaiveDate::from_ymd_opt(dt.year(), dt.month(), dt.day()).unwrap(); + let value = week_size.value(&date, &interp_day); + data.push(value); + + dt += TimeDelta::days(1); + } + data + } + + /// Test a leap year with a profile of 52 values + #[test] + fn test_52_values() { + let profile = [ + 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, + 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, + 38.0, 39.0, 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0, + ]; + let week_size = WeeklyProfileValues::FiftyTwo(profile); + + // No interpolation + let expected_values_interp_none = [ + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, + 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, + 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, + 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 11.0, 11.0, 11.0, 11.0, 11.0, 11.0, 11.0, 12.0, 12.0, 12.0, 12.0, + 12.0, 12.0, 12.0, 13.0, 13.0, 13.0, 13.0, 13.0, 13.0, 13.0, 14.0, 14.0, 14.0, 14.0, 14.0, 14.0, 14.0, 15.0, + 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 16.0, 16.0, 16.0, 16.0, 16.0, 16.0, 16.0, 17.0, 17.0, 17.0, 17.0, 17.0, + 17.0, 17.0, 18.0, 18.0, 18.0, 18.0, 18.0, 18.0, 18.0, 19.0, 19.0, 19.0, 19.0, 19.0, 19.0, 19.0, 20.0, 20.0, + 20.0, 20.0, 20.0, 20.0, 20.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 22.0, 22.0, 22.0, 22.0, 22.0, 22.0, + 22.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 25.0, 25.0, 25.0, + 25.0, 25.0, 25.0, 25.0, 26.0, 26.0, 26.0, 26.0, 26.0, 26.0, 26.0, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, + 28.0, 28.0, 28.0, 28.0, 28.0, 28.0, 28.0, 29.0, 29.0, 29.0, 29.0, 29.0, 29.0, 29.0, 30.0, 30.0, 30.0, 30.0, + 30.0, 30.0, 30.0, 31.0, 31.0, 31.0, 31.0, 31.0, 31.0, 31.0, 32.0, 32.0, 32.0, 32.0, 32.0, 32.0, 32.0, 33.0, + 33.0, 33.0, 33.0, 33.0, 33.0, 33.0, 34.0, 34.0, 34.0, 34.0, 34.0, 34.0, 34.0, 35.0, 35.0, 35.0, 35.0, 35.0, + 35.0, 35.0, 36.0, 36.0, 36.0, 36.0, 36.0, 36.0, 36.0, 37.0, 37.0, 37.0, 37.0, 37.0, 37.0, 37.0, 38.0, 38.0, + 38.0, 38.0, 38.0, 38.0, 38.0, 39.0, 39.0, 39.0, 39.0, 39.0, 39.0, 39.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, + 40.0, 41.0, 41.0, 41.0, 41.0, 41.0, 41.0, 41.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 43.0, 43.0, 43.0, + 43.0, 43.0, 43.0, 43.0, 44.0, 44.0, 44.0, 44.0, 44.0, 44.0, 44.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, + 46.0, 46.0, 46.0, 46.0, 46.0, 46.0, 46.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 48.0, 48.0, 48.0, 48.0, + 48.0, 48.0, 48.0, 49.0, 49.0, 49.0, 49.0, 49.0, 49.0, 49.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 51.0, + 51.0, 51.0, 51.0, 51.0, 51.0, 51.0, 52.0, 52.0, 52.0, 52.0, 52.0, 52.0, 52.0, 52.0, 52.0, + ]; + let values_interp_none = collect(&week_size, None); + assert_approx_array_eq(&values_interp_none, &expected_values_interp_none); + + // WeeklyInterpDay::First + let expected_values_interp_first = [ + 1.0, 1.14286, 1.28571, 1.42857, 1.57143, 1.71429, 1.85714, 2.0, 2.14286, 2.28571, 2.42857, 2.57143, + 2.71429, 2.85714, 3.0, 3.14286, 3.28571, 3.42857, 3.57143, 3.71429, 3.85714, 4.0, 4.14286, 4.28571, + 4.42857, 4.57143, 4.71429, 4.85714, 5.0, 5.14286, 5.28571, 5.42857, 5.57143, 5.71429, 5.85714, 6.0, + 6.14286, 6.28571, 6.42857, 6.57143, 6.71429, 6.85714, 7.0, 7.14286, 7.28571, 7.42857, 7.57143, 7.71429, + 7.85714, 8.0, 8.14286, 8.28571, 8.42857, 8.57143, 8.71429, 8.85714, 9.0, 9.14286, 9.28571, 9.42857, + 9.57143, 9.71429, 9.85714, 10.0, 10.14286, 10.28571, 10.42857, 10.57143, 10.71429, 10.85714, 11.0, + 11.14286, 11.28571, 11.42857, 11.57143, 11.71429, 11.85714, 12.0, 12.14286, 12.28571, 12.42857, 12.57143, + 12.71429, 12.85714, 13.0, 13.14286, 13.28571, 13.42857, 13.57143, 13.71429, 13.85714, 14.0, 14.14286, + 14.28571, 14.42857, 14.57143, 14.71429, 14.85714, 15.0, 15.14286, 15.28571, 15.42857, 15.57143, 15.71429, + 15.85714, 16.0, 16.14286, 16.28571, 16.42857, 16.57143, 16.71429, 16.85714, 17.0, 17.14286, 17.28571, + 17.42857, 17.57143, 17.71429, 17.85714, 18.0, 18.14286, 18.28571, 18.42857, 18.57143, 18.71429, 18.85714, + 19.0, 19.14286, 19.28571, 19.42857, 19.57143, 19.71429, 19.85714, 20.0, 20.14286, 20.28571, 20.42857, + 20.57143, 20.71429, 20.85714, 21.0, 21.14286, 21.28571, 21.42857, 21.57143, 21.71429, 21.85714, 22.0, + 22.14286, 22.28571, 22.42857, 22.57143, 22.71429, 22.85714, 23.0, 23.14286, 23.28571, 23.42857, 23.57143, + 23.71429, 23.85714, 24.0, 24.14286, 24.28571, 24.42857, 24.57143, 24.71429, 24.85714, 25.0, 25.14286, + 25.28571, 25.42857, 25.57143, 25.71429, 25.85714, 26.0, 26.14286, 26.28571, 26.42857, 26.57143, 26.71429, + 26.85714, 27.0, 27.14286, 27.28571, 27.42857, 27.57143, 27.71429, 27.85714, 28.0, 28.14286, 28.28571, + 28.42857, 28.57143, 28.71429, 28.85714, 29.0, 29.14286, 29.28571, 29.42857, 29.57143, 29.71429, 29.85714, + 30.0, 30.14286, 30.28571, 30.42857, 30.57143, 30.71429, 30.85714, 31.0, 31.14286, 31.28571, 31.42857, + 31.57143, 31.71429, 31.85714, 32.0, 32.14286, 32.28571, 32.42857, 32.57143, 32.71429, 32.85714, 33.0, + 33.14286, 33.28571, 33.42857, 33.57143, 33.71429, 33.85714, 34.0, 34.14286, 34.28571, 34.42857, 34.57143, + 34.71429, 34.85714, 35.0, 35.14286, 35.28571, 35.42857, 35.57143, 35.71429, 35.85714, 36.0, 36.14286, + 36.28571, 36.42857, 36.57143, 36.71429, 36.85714, 37.0, 37.14286, 37.28571, 37.42857, 37.57143, 37.71429, + 37.85714, 38.0, 38.14286, 38.28571, 38.42857, 38.57143, 38.71429, 38.85714, 39.0, 39.14286, 39.28571, + 39.42857, 39.57143, 39.71429, 39.85714, 40.0, 40.14286, 40.28571, 40.42857, 40.57143, 40.71429, 40.85714, + 41.0, 41.14286, 41.28571, 41.42857, 41.57143, 41.71429, 41.85714, 42.0, 42.14286, 42.28571, 42.42857, + 42.57143, 42.71429, 42.85714, 43.0, 43.14286, 43.28571, 43.42857, 43.57143, 43.71429, 43.85714, 44.0, + 44.14286, 44.28571, 44.42857, 44.57143, 44.71429, 44.85714, 45.0, 45.14286, 45.28571, 45.42857, 45.57143, + 45.71429, 45.85714, 46.0, 46.14286, 46.28571, 46.42857, 46.57143, 46.71429, 46.85714, 47.0, 47.14286, + 47.28571, 47.42857, 47.57143, 47.71429, 47.85714, 48.0, 48.14286, 48.28571, 48.42857, 48.57143, 48.71429, + 48.85714, 49.0, 49.14286, 49.28571, 49.42857, 49.57143, 49.71429, 49.85714, 50.0, 50.14286, 50.28571, + 50.42857, 50.57143, 50.71429, 50.85714, 51.0, 51.14286, 51.28571, 51.42857, 51.57143, 51.71429, 51.85714, + 52.0, 44.71429, 37.42857, 30.14286, 22.85714, 15.57143, 8.28571, 52.0, 44.71429, 37.42857, + ]; + let values_interp_first = collect(&week_size, Some(WeeklyInterpDay::First)); + assert_approx_array_eq(&values_interp_first, &expected_values_interp_first); + + // WeeklyInterpDay::Last + let expected_values_interp_last = [ + 52.0, 44.71429, 37.42857, 30.14286, 22.85714, 15.57143, 8.28571, 1.0, 1.14286, 1.28571, 1.42857, 1.57143, + 1.71429, 1.85714, 2.0, 2.14286, 2.28571, 2.42857, 2.57143, 2.71429, 2.85714, 3.0, 3.14286, 3.28571, + 3.42857, 3.57143, 3.71429, 3.85714, 4.0, 4.14286, 4.28571, 4.42857, 4.57143, 4.71429, 4.85714, 5.0, + 5.14286, 5.28571, 5.42857, 5.57143, 5.71429, 5.85714, 6.0, 6.14286, 6.28571, 6.42857, 6.57143, 6.71429, + 6.85714, 7.0, 7.14286, 7.28571, 7.42857, 7.57143, 7.71429, 7.85714, 8.0, 8.14286, 8.28571, 8.42857, + 8.57143, 8.71429, 8.85714, 9.0, 9.14286, 9.28571, 9.42857, 9.57143, 9.71429, 9.85714, 10.0, 10.14286, + 10.28571, 10.42857, 10.57143, 10.71429, 10.85714, 11.0, 11.14286, 11.28571, 11.42857, 11.57143, 11.71429, + 11.85714, 12.0, 12.14286, 12.28571, 12.42857, 12.57143, 12.71429, 12.85714, 13.0, 13.14286, 13.28571, + 13.42857, 13.57143, 13.71429, 13.85714, 14.0, 14.14286, 14.28571, 14.42857, 14.57143, 14.71429, 14.85714, + 15.0, 15.14286, 15.28571, 15.42857, 15.57143, 15.71429, 15.85714, 16.0, 16.14286, 16.28571, 16.42857, + 16.57143, 16.71429, 16.85714, 17.0, 17.14286, 17.28571, 17.42857, 17.57143, 17.71429, 17.85714, 18.0, + 18.14286, 18.28571, 18.42857, 18.57143, 18.71429, 18.85714, 19.0, 19.14286, 19.28571, 19.42857, 19.57143, + 19.71429, 19.85714, 20.0, 20.14286, 20.28571, 20.42857, 20.57143, 20.71429, 20.85714, 21.0, 21.14286, + 21.28571, 21.42857, 21.57143, 21.71429, 21.85714, 22.0, 22.14286, 22.28571, 22.42857, 22.57143, 22.71429, + 22.85714, 23.0, 23.14286, 23.28571, 23.42857, 23.57143, 23.71429, 23.85714, 24.0, 24.14286, 24.28571, + 24.42857, 24.57143, 24.71429, 24.85714, 25.0, 25.14286, 25.28571, 25.42857, 25.57143, 25.71429, 25.85714, + 26.0, 26.14286, 26.28571, 26.42857, 26.57143, 26.71429, 26.85714, 27.0, 27.14286, 27.28571, 27.42857, + 27.57143, 27.71429, 27.85714, 28.0, 28.14286, 28.28571, 28.42857, 28.57143, 28.71429, 28.85714, 29.0, + 29.14286, 29.28571, 29.42857, 29.57143, 29.71429, 29.85714, 30.0, 30.14286, 30.28571, 30.42857, 30.57143, + 30.71429, 30.85714, 31.0, 31.14286, 31.28571, 31.42857, 31.57143, 31.71429, 31.85714, 32.0, 32.14286, + 32.28571, 32.42857, 32.57143, 32.71429, 32.85714, 33.0, 33.14286, 33.28571, 33.42857, 33.57143, 33.71429, + 33.85714, 34.0, 34.14286, 34.28571, 34.42857, 34.57143, 34.71429, 34.85714, 35.0, 35.14286, 35.28571, + 35.42857, 35.57143, 35.71429, 35.85714, 36.0, 36.14286, 36.28571, 36.42857, 36.57143, 36.71429, 36.85714, + 37.0, 37.14286, 37.28571, 37.42857, 37.57143, 37.71429, 37.85714, 38.0, 38.14286, 38.28571, 38.42857, + 38.57143, 38.71429, 38.85714, 39.0, 39.14286, 39.28571, 39.42857, 39.57143, 39.71429, 39.85714, 40.0, + 40.14286, 40.28571, 40.42857, 40.57143, 40.71429, 40.85714, 41.0, 41.14286, 41.28571, 41.42857, 41.57143, + 41.71429, 41.85714, 42.0, 42.14286, 42.28571, 42.42857, 42.57143, 42.71429, 42.85714, 43.0, 43.14286, + 43.28571, 43.42857, 43.57143, 43.71429, 43.85714, 44.0, 44.14286, 44.28571, 44.42857, 44.57143, 44.71429, + 44.85714, 45.0, 45.14286, 45.28571, 45.42857, 45.57143, 45.71429, 45.85714, 46.0, 46.14286, 46.28571, + 46.42857, 46.57143, 46.71429, 46.85714, 47.0, 47.14286, 47.28571, 47.42857, 47.57143, 47.71429, 47.85714, + 48.0, 48.14286, 48.28571, 48.42857, 48.57143, 48.71429, 48.85714, 49.0, 49.14286, 49.28571, 49.42857, + 49.57143, 49.71429, 49.85714, 50.0, 50.14286, 50.28571, 50.42857, 50.57143, 50.71429, 50.85714, 51.0, + 51.14286, 51.28571, 51.42857, 51.57143, 51.71429, 51.85714, 52.0, 51.875, + ]; + let values_interp_none = collect(&week_size, Some(WeeklyInterpDay::Last)); + assert_approx_array_eq(&values_interp_none, &expected_values_interp_last); + } + + /// Test a leap year with a profile of 53 values + #[test] + fn test_53_values() { + let profile = [ + 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, + 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, + 38.0, 39.0, 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0, 53.0, + ]; + let week_size = WeeklyProfileValues::FiftyThree(profile); + + // No interpolation + let expected_values_interp_none = [ + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, + 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0, + 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 8.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, 9.0, + 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 11.0, 11.0, 11.0, 11.0, 11.0, 11.0, 11.0, 12.0, 12.0, 12.0, 12.0, + 12.0, 12.0, 12.0, 13.0, 13.0, 13.0, 13.0, 13.0, 13.0, 13.0, 14.0, 14.0, 14.0, 14.0, 14.0, 14.0, 14.0, 15.0, + 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 16.0, 16.0, 16.0, 16.0, 16.0, 16.0, 16.0, 17.0, 17.0, 17.0, 17.0, 17.0, + 17.0, 17.0, 18.0, 18.0, 18.0, 18.0, 18.0, 18.0, 18.0, 19.0, 19.0, 19.0, 19.0, 19.0, 19.0, 19.0, 20.0, 20.0, + 20.0, 20.0, 20.0, 20.0, 20.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 21.0, 22.0, 22.0, 22.0, 22.0, 22.0, 22.0, + 22.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 25.0, 25.0, 25.0, + 25.0, 25.0, 25.0, 25.0, 26.0, 26.0, 26.0, 26.0, 26.0, 26.0, 26.0, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, 27.0, + 28.0, 28.0, 28.0, 28.0, 28.0, 28.0, 28.0, 29.0, 29.0, 29.0, 29.0, 29.0, 29.0, 29.0, 30.0, 30.0, 30.0, 30.0, + 30.0, 30.0, 30.0, 31.0, 31.0, 31.0, 31.0, 31.0, 31.0, 31.0, 32.0, 32.0, 32.0, 32.0, 32.0, 32.0, 32.0, 33.0, + 33.0, 33.0, 33.0, 33.0, 33.0, 33.0, 34.0, 34.0, 34.0, 34.0, 34.0, 34.0, 34.0, 35.0, 35.0, 35.0, 35.0, 35.0, + 35.0, 35.0, 36.0, 36.0, 36.0, 36.0, 36.0, 36.0, 36.0, 37.0, 37.0, 37.0, 37.0, 37.0, 37.0, 37.0, 38.0, 38.0, + 38.0, 38.0, 38.0, 38.0, 38.0, 39.0, 39.0, 39.0, 39.0, 39.0, 39.0, 39.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, + 40.0, 41.0, 41.0, 41.0, 41.0, 41.0, 41.0, 41.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 42.0, 43.0, 43.0, 43.0, + 43.0, 43.0, 43.0, 43.0, 44.0, 44.0, 44.0, 44.0, 44.0, 44.0, 44.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, + 46.0, 46.0, 46.0, 46.0, 46.0, 46.0, 46.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 48.0, 48.0, 48.0, 48.0, + 48.0, 48.0, 48.0, 49.0, 49.0, 49.0, 49.0, 49.0, 49.0, 49.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 51.0, + 51.0, 51.0, 51.0, 51.0, 51.0, 51.0, 52.0, 52.0, 52.0, 52.0, 52.0, 52.0, 52.0, 53.0, 53.0, + ]; + let values_interp_none = collect(&week_size, None); + assert_approx_array_eq(&values_interp_none, &expected_values_interp_none); + + // WeeklyInterpDay::First + let expected_values_interp_first = [ + 1.0, 1.14286, 1.28571, 1.42857, 1.57143, 1.71429, 1.85714, 2.0, 2.14286, 2.28571, 2.42857, 2.57143, + 2.71429, 2.85714, 3.0, 3.14286, 3.28571, 3.42857, 3.57143, 3.71429, 3.85714, 4.0, 4.14286, 4.28571, + 4.42857, 4.57143, 4.71429, 4.85714, 5.0, 5.14286, 5.28571, 5.42857, 5.57143, 5.71429, 5.85714, 6.0, + 6.14286, 6.28571, 6.42857, 6.57143, 6.71429, 6.85714, 7.0, 7.14286, 7.28571, 7.42857, 7.57143, 7.71429, + 7.85714, 8.0, 8.14286, 8.28571, 8.42857, 8.57143, 8.71429, 8.85714, 9.0, 9.14286, 9.28571, 9.42857, + 9.57143, 9.71429, 9.85714, 10.0, 10.14286, 10.28571, 10.42857, 10.57143, 10.71429, 10.85714, 11.0, + 11.14286, 11.28571, 11.42857, 11.57143, 11.71429, 11.85714, 12.0, 12.14286, 12.28571, 12.42857, 12.57143, + 12.71429, 12.85714, 13.0, 13.14286, 13.28571, 13.42857, 13.57143, 13.71429, 13.85714, 14.0, 14.14286, + 14.28571, 14.42857, 14.57143, 14.71429, 14.85714, 15.0, 15.14286, 15.28571, 15.42857, 15.57143, 15.71429, + 15.85714, 16.0, 16.14286, 16.28571, 16.42857, 16.57143, 16.71429, 16.85714, 17.0, 17.14286, 17.28571, + 17.42857, 17.57143, 17.71429, 17.85714, 18.0, 18.14286, 18.28571, 18.42857, 18.57143, 18.71429, 18.85714, + 19.0, 19.14286, 19.28571, 19.42857, 19.57143, 19.71429, 19.85714, 20.0, 20.14286, 20.28571, 20.42857, + 20.57143, 20.71429, 20.85714, 21.0, 21.14286, 21.28571, 21.42857, 21.57143, 21.71429, 21.85714, 22.0, + 22.14286, 22.28571, 22.42857, 22.57143, 22.71429, 22.85714, 23.0, 23.14286, 23.28571, 23.42857, 23.57143, + 23.71429, 23.85714, 24.0, 24.14286, 24.28571, 24.42857, 24.57143, 24.71429, 24.85714, 25.0, 25.14286, + 25.28571, 25.42857, 25.57143, 25.71429, 25.85714, 26.0, 26.14286, 26.28571, 26.42857, 26.57143, 26.71429, + 26.85714, 27.0, 27.14286, 27.28571, 27.42857, 27.57143, 27.71429, 27.85714, 28.0, 28.14286, 28.28571, + 28.42857, 28.57143, 28.71429, 28.85714, 29.0, 29.14286, 29.28571, 29.42857, 29.57143, 29.71429, 29.85714, + 30.0, 30.14286, 30.28571, 30.42857, 30.57143, 30.71429, 30.85714, 31.0, 31.14286, 31.28571, 31.42857, + 31.57143, 31.71429, 31.85714, 32.0, 32.14286, 32.28571, 32.42857, 32.57143, 32.71429, 32.85714, 33.0, + 33.14286, 33.28571, 33.42857, 33.57143, 33.71429, 33.85714, 34.0, 34.14286, 34.28571, 34.42857, 34.57143, + 34.71429, 34.85714, 35.0, 35.14286, 35.28571, 35.42857, 35.57143, 35.71429, 35.85714, 36.0, 36.14286, + 36.28571, 36.42857, 36.57143, 36.71429, 36.85714, 37.0, 37.14286, 37.28571, 37.42857, 37.57143, 37.71429, + 37.85714, 38.0, 38.14286, 38.28571, 38.42857, 38.57143, 38.71429, 38.85714, 39.0, 39.14286, 39.28571, + 39.42857, 39.57143, 39.71429, 39.85714, 40.0, 40.14286, 40.28571, 40.42857, 40.57143, 40.71429, 40.85714, + 41.0, 41.14286, 41.28571, 41.42857, 41.57143, 41.71429, 41.85714, 42.0, 42.14286, 42.28571, 42.42857, + 42.57143, 42.71429, 42.85714, 43.0, 43.14286, 43.28571, 43.42857, 43.57143, 43.71429, 43.85714, 44.0, + 44.14286, 44.28571, 44.42857, 44.57143, 44.71429, 44.85714, 45.0, 45.14286, 45.28571, 45.42857, 45.57143, + 45.71429, 45.85714, 46.0, 46.14286, 46.28571, 46.42857, 46.57143, 46.71429, 46.85714, 47.0, 47.14286, + 47.28571, 47.42857, 47.57143, 47.71429, 47.85714, 48.0, 48.14286, 48.28571, 48.42857, 48.57143, 48.71429, + 48.85714, 49.0, 49.14286, 49.28571, 49.42857, 49.57143, 49.71429, 49.85714, 50.0, 50.14286, 50.28571, + 50.42857, 50.57143, 50.71429, 50.85714, 51.0, 51.14286, 51.28571, 51.42857, 51.57143, 51.71429, 51.85714, + 52.0, 52.14286, 52.28571, 52.42857, 52.57143, 52.71429, 52.85714, 53.0, 45.57143, + ]; + let values_interp_first = collect(&week_size, Some(WeeklyInterpDay::First)); + assert_approx_array_eq(&values_interp_first, &expected_values_interp_first); + + // WeeklyInterpDay::Last + let expected_values_interp_last = [ + 53.0, 45.57143, 38.14286, 30.71429, 23.28571, 15.85714, 8.42857, 1.0, 1.14286, 1.28571, 1.42857, 1.57143, + 1.71429, 1.85714, 2.0, 2.14286, 2.28571, 2.42857, 2.57143, 2.71429, 2.85714, 3.0, 3.14286, 3.28571, + 3.42857, 3.57143, 3.71429, 3.85714, 4.0, 4.14286, 4.28571, 4.42857, 4.57143, 4.71429, 4.85714, 5.0, + 5.14286, 5.28571, 5.42857, 5.57143, 5.71429, 5.85714, 6.0, 6.14286, 6.28571, 6.42857, 6.57143, 6.71429, + 6.85714, 7.0, 7.14286, 7.28571, 7.42857, 7.57143, 7.71429, 7.85714, 8.0, 8.14286, 8.28571, 8.42857, + 8.57143, 8.71429, 8.85714, 9.0, 9.14286, 9.28571, 9.42857, 9.57143, 9.71429, 9.85714, 10.0, 10.14286, + 10.28571, 10.42857, 10.57143, 10.71429, 10.85714, 11.0, 11.14286, 11.28571, 11.42857, 11.57143, 11.71429, + 11.85714, 12.0, 12.14286, 12.28571, 12.42857, 12.57143, 12.71429, 12.85714, 13.0, 13.14286, 13.28571, + 13.42857, 13.57143, 13.71429, 13.85714, 14.0, 14.14286, 14.28571, 14.42857, 14.57143, 14.71429, 14.85714, + 15.0, 15.14286, 15.28571, 15.42857, 15.57143, 15.71429, 15.85714, 16.0, 16.14286, 16.28571, 16.42857, + 16.57143, 16.71429, 16.85714, 17.0, 17.14286, 17.28571, 17.42857, 17.57143, 17.71429, 17.85714, 18.0, + 18.14286, 18.28571, 18.42857, 18.57143, 18.71429, 18.85714, 19.0, 19.14286, 19.28571, 19.42857, 19.57143, + 19.71429, 19.85714, 20.0, 20.14286, 20.28571, 20.42857, 20.57143, 20.71429, 20.85714, 21.0, 21.14286, + 21.28571, 21.42857, 21.57143, 21.71429, 21.85714, 22.0, 22.14286, 22.28571, 22.42857, 22.57143, 22.71429, + 22.85714, 23.0, 23.14286, 23.28571, 23.42857, 23.57143, 23.71429, 23.85714, 24.0, 24.14286, 24.28571, + 24.42857, 24.57143, 24.71429, 24.85714, 25.0, 25.14286, 25.28571, 25.42857, 25.57143, 25.71429, 25.85714, + 26.0, 26.14286, 26.28571, 26.42857, 26.57143, 26.71429, 26.85714, 27.0, 27.14286, 27.28571, 27.42857, + 27.57143, 27.71429, 27.85714, 28.0, 28.14286, 28.28571, 28.42857, 28.57143, 28.71429, 28.85714, 29.0, + 29.14286, 29.28571, 29.42857, 29.57143, 29.71429, 29.85714, 30.0, 30.14286, 30.28571, 30.42857, 30.57143, + 30.71429, 30.85714, 31.0, 31.14286, 31.28571, 31.42857, 31.57143, 31.71429, 31.85714, 32.0, 32.14286, + 32.28571, 32.42857, 32.57143, 32.71429, 32.85714, 33.0, 33.14286, 33.28571, 33.42857, 33.57143, 33.71429, + 33.85714, 34.0, 34.14286, 34.28571, 34.42857, 34.57143, 34.71429, 34.85714, 35.0, 35.14286, 35.28571, + 35.42857, 35.57143, 35.71429, 35.85714, 36.0, 36.14286, 36.28571, 36.42857, 36.57143, 36.71429, 36.85714, + 37.0, 37.14286, 37.28571, 37.42857, 37.57143, 37.71429, 37.85714, 38.0, 38.14286, 38.28571, 38.42857, + 38.57143, 38.71429, 38.85714, 39.0, 39.14286, 39.28571, 39.42857, 39.57143, 39.71429, 39.85714, 40.0, + 40.14286, 40.28571, 40.42857, 40.57143, 40.71429, 40.85714, 41.0, 41.14286, 41.28571, 41.42857, 41.57143, + 41.71429, 41.85714, 42.0, 42.14286, 42.28571, 42.42857, 42.57143, 42.71429, 42.85714, 43.0, 43.14286, + 43.28571, 43.42857, 43.57143, 43.71429, 43.85714, 44.0, 44.14286, 44.28571, 44.42857, 44.57143, 44.71429, + 44.85714, 45.0, 45.14286, 45.28571, 45.42857, 45.57143, 45.71429, 45.85714, 46.0, 46.14286, 46.28571, + 46.42857, 46.57143, 46.71429, 46.85714, 47.0, 47.14286, 47.28571, 47.42857, 47.57143, 47.71429, 47.85714, + 48.0, 48.14286, 48.28571, 48.42857, 48.57143, 48.71429, 48.85714, 49.0, 49.14286, 49.28571, 49.42857, + 49.57143, 49.71429, 49.85714, 50.0, 50.14286, 50.28571, 50.42857, 50.57143, 50.71429, 50.85714, 51.0, + 51.14286, 51.28571, 51.42857, 51.57143, 51.71429, 51.85714, 52.0, 52.125, + ]; + let values_interp_none = collect(&week_size, Some(WeeklyInterpDay::Last)); + assert_approx_array_eq(&values_interp_none, &expected_values_interp_last); + } +} diff --git a/pywr-core/src/parameters/py.rs b/pywr-core/src/parameters/py.rs index 3927511d..28714beb 100644 --- a/pywr-core/src/parameters/py.rs +++ b/pywr-core/src/parameters/py.rs @@ -1,11 +1,11 @@ use super::{IndexValue, Parameter, ParameterMeta, PywrError, Timestep}; use crate::metric::Metric; use crate::network::Network; -use crate::parameters::{downcast_internal_state, MultiValueParameter}; +use crate::parameters::{downcast_internal_state_mut, MultiValueParameter}; use crate::scenario::ScenarioIndex; use crate::state::{MultiValue, ParameterState, State}; use pyo3::prelude::*; -use pyo3::types::{IntoPyDict, PyDate, PyDict, PyFloat, PyLong, PyTuple}; +use pyo3::types::{IntoPyDict, PyDict, PyFloat, PyLong, PyTuple}; use std::any::Any; use std::collections::HashMap; @@ -113,22 +113,17 @@ impl Parameter for PyParameter { state: &State, internal_state: &mut Option>, ) -> Result { - let internal = downcast_internal_state::(internal_state); + let internal = downcast_internal_state_mut::(internal_state); let value: f64 = Python::with_gil(|py| { - let date = PyDate::new( - py, - timestep.date.year(), - timestep.date.month() as u8, - timestep.date.day(), - )?; + let date = timestep.date.into_py(py); let si = scenario_index.index.into_py(py); let metric_dict = self.get_metrics_dict(model, state, py)?; let index_dict = self.get_indices_dict(state, py)?; - let args = PyTuple::new(py, [date, si.as_ref(py), metric_dict, index_dict]); + let args = PyTuple::new(py, [date.as_ref(py), si.as_ref(py), metric_dict, index_dict]); internal.user_obj.call_method1(py, "calc", args)?.extract(py) }) @@ -145,24 +140,19 @@ impl Parameter for PyParameter { state: &State, internal_state: &mut Option>, ) -> Result<(), PywrError> { - let internal = downcast_internal_state::(internal_state); + let internal = downcast_internal_state_mut::(internal_state); Python::with_gil(|py| { // Only do this if the object has an "after" method defined. if internal.user_obj.getattr(py, "after").is_ok() { - let date = PyDate::new( - py, - timestep.date.year(), - timestep.date.month() as u8, - timestep.date.day(), - )?; + let date = timestep.date.into_py(py); let si = scenario_index.index.into_py(py); let metric_dict = self.get_metrics_dict(model, state, py)?; let index_dict = self.get_indices_dict(state, py)?; - let args = PyTuple::new(py, [date, si.as_ref(py), metric_dict, index_dict]); + let args = PyTuple::new(py, [date.as_ref(py), si.as_ref(py), metric_dict, index_dict]); internal.user_obj.call_method1(py, "after", args)?; } @@ -215,23 +205,17 @@ impl MultiValueParameter for PyParameter { state: &State, internal_state: &mut Option>, ) -> Result { - let internal = downcast_internal_state::(internal_state); + let internal = downcast_internal_state_mut::(internal_state); let value: MultiValue = Python::with_gil(|py| { - let date = PyDate::new( - py, - timestep.date.year(), - timestep.date.month() as u8, - timestep.date.day(), - ) - .map_err(|e: PyErr| PywrError::PythonError(e.to_string()))?; + let date = timestep.date.into_py(py); let si = scenario_index.index.into_py(py); let metric_dict = self.get_metrics_dict(model, state, py)?; let index_dict = self.get_indices_dict(state, py)?; - let args = PyTuple::new(py, [date, si.as_ref(py), metric_dict, index_dict]); + let args = PyTuple::new(py, [date.as_ref(py), si.as_ref(py), metric_dict, index_dict]); let py_values: HashMap = internal .user_obj @@ -277,24 +261,19 @@ impl MultiValueParameter for PyParameter { state: &State, internal_state: &mut Option>, ) -> Result<(), PywrError> { - let internal = downcast_internal_state::(internal_state); + let internal = downcast_internal_state_mut::(internal_state); Python::with_gil(|py| { // Only do this if the object has an "after" method defined. if internal.user_obj.getattr(py, "after").is_ok() { - let date = PyDate::new( - py, - timestep.date.year(), - timestep.date.month() as u8, - timestep.date.day(), - )?; + let date = timestep.date.into_py(py); let si = scenario_index.index.into_py(py); let metric_dict = self.get_metrics_dict(model, state, py)?; let index_dict = self.get_indices_dict(state, py)?; - let args = PyTuple::new(py, [date, si.as_ref(py), metric_dict, index_dict]); + let args = PyTuple::new(py, [date.as_ref(py), si.as_ref(py), metric_dict, index_dict]); internal.user_obj.call_method1(py, "after", args)?; } @@ -309,8 +288,10 @@ impl MultiValueParameter for PyParameter { #[cfg(test)] mod tests { use super::*; + use crate::state::StateBuilder; use crate::test_utils::default_timestepper; use crate::timestep::TimeDomain; + use chrono::Datelike; use float_cmp::assert_approx_eq; #[test] @@ -344,7 +325,7 @@ class MyParameter: let param = PyParameter::new("my-parameter", class, args, kwargs, &HashMap::new(), &HashMap::new()); let timestepper = default_timestepper(); - let time: TimeDomain = timestepper.into(); + let time: TimeDomain = TimeDomain::try_from(timestepper).unwrap(); let timesteps = time.timesteps(); let scenario_indices = [ @@ -358,7 +339,7 @@ class MyParameter: }, ]; - let state = State::new(vec![], 0, vec![], 1, 0, 0, 0, 0); + let state = StateBuilder::new(vec![], 0).with_value_parameters(1).build(); let mut internal_p_states: Vec<_> = scenario_indices .iter() @@ -413,7 +394,7 @@ class MyParameter: let param = PyParameter::new("my-parameter", class, args, kwargs, &HashMap::new(), &HashMap::new()); let timestepper = default_timestepper(); - let time: TimeDomain = timestepper.into(); + let time: TimeDomain = TimeDomain::try_from(timestepper).unwrap(); let timesteps = time.timesteps(); let scenario_indices = [ @@ -427,7 +408,7 @@ class MyParameter: }, ]; - let state = State::new(vec![], 0, vec![], 1, 0, 0, 0, 0); + let state = StateBuilder::new(vec![], 0).with_value_parameters(1).build(); let mut internal_p_states: Vec<_> = scenario_indices .iter() diff --git a/pywr-core/src/parameters/rhai.rs b/pywr-core/src/parameters/rhai.rs index 2927c080..8fcfbb5f 100644 --- a/pywr-core/src/parameters/rhai.rs +++ b/pywr-core/src/parameters/rhai.rs @@ -1,9 +1,10 @@ use super::{IndexValue, Parameter, ParameterMeta, PywrError, Timestep}; use crate::metric::Metric; use crate::network::Network; -use crate::parameters::downcast_internal_state; +use crate::parameters::downcast_internal_state_mut; use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; +use chrono::Datelike; use rhai::{Dynamic, Engine, Map, Scope, AST}; use std::any::Any; use std::collections::HashMap; @@ -89,7 +90,7 @@ impl Parameter for RhaiParameter { state: &State, internal_state: &mut Option>, ) -> Result { - let internal = downcast_internal_state::(internal_state); + let internal = downcast_internal_state_mut::(internal_state); let metric_values = self .metrics @@ -119,6 +120,7 @@ impl Parameter for RhaiParameter { #[cfg(test)] mod tests { use super::*; + use crate::state::StateBuilder; use crate::test_utils::default_timestepper; use crate::timestep::TimeDomain; use float_cmp::assert_approx_eq; @@ -150,7 +152,7 @@ mod tests { ); let timestepper = default_timestepper(); - let time: TimeDomain = timestepper.into(); + let time: TimeDomain = TimeDomain::try_from(timestepper).unwrap(); let timesteps = time.timesteps(); let scenario_indices = [ @@ -164,7 +166,7 @@ mod tests { }, ]; - let state = State::new(vec![], 0, vec![], 1, 0, 0, 0, 0); + let state = StateBuilder::new(vec![], 0).with_value_parameters(1).build(); let mut internal_p_states: Vec<_> = scenario_indices .iter() diff --git a/pywr-core/src/parameters/threshold.rs b/pywr-core/src/parameters/threshold.rs index ccc16f2c..6d02fc05 100644 --- a/pywr-core/src/parameters/threshold.rs +++ b/pywr-core/src/parameters/threshold.rs @@ -1,6 +1,6 @@ use crate::metric::Metric; use crate::network::Network; -use crate::parameters::{downcast_internal_state, IndexParameter, ParameterMeta}; +use crate::parameters::{downcast_internal_state_mut, IndexParameter, ParameterMeta}; use crate::scenario::ScenarioIndex; use crate::state::{ParameterState, State}; use crate::timestep::Timestep; @@ -74,7 +74,7 @@ impl IndexParameter for ThresholdParameter { internal_state: &mut Option>, ) -> Result { // Downcast the internal state to the correct type - let previously_activated = downcast_internal_state::(internal_state); + let previously_activated = downcast_internal_state_mut::(internal_state); // Return early if ratchet has been hit if self.ratchet & *previously_activated { diff --git a/pywr-core/src/recorders/aggregator.rs b/pywr-core/src/recorders/aggregator.rs index a182da49..7f5af08f 100644 --- a/pywr-core/src/recorders/aggregator.rs +++ b/pywr-core/src/recorders/aggregator.rs @@ -1,4 +1,5 @@ -use time::{Date, Duration, Month}; +use crate::timestep::PywrDuration; +use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime}; #[derive(Clone, Debug)] pub enum AggregationFrequency { @@ -7,31 +8,37 @@ pub enum AggregationFrequency { } impl AggregationFrequency { - fn is_date_in_period(&self, period_start: &Date, date: &Date) -> bool { + fn is_date_in_period(&self, period_start: &NaiveDateTime, date: &NaiveDateTime) -> bool { match self { Self::Monthly => (period_start.year() == date.year()) && (period_start.month() == date.month()), Self::Annual => period_start.year() == date.year(), } } - fn start_of_next_period(&self, current_date: &Date) -> Date { + fn start_of_next_period(&self, current_date: &NaiveDateTime) -> NaiveDateTime { match self { Self::Monthly => { + let current_month = current_date.month(); // Increment the year if we're in December - let year = if current_date.month() == Month::December { + let year = if current_month == 12 { current_date.year() + 1 } else { current_date.year() }; + let next_month = (current_month % 12) + 1; // 1st of the next month // SAFETY: This should be safe to unwrap as it will always create a valid date unless // we are at the limit of dates that are representable. - Date::from_calendar_date(year, current_date.month().next(), 1).unwrap() + let date = NaiveDate::from_ymd_opt(year, next_month, 1).unwrap(); + NaiveDateTime::new(date, NaiveTime::default()) + } + Self::Annual => { + // 1st of January in the next year + // SAFETY: This should be safe to unwrap as it will always create a valid date unless + // we are at the limit of dates that are representable. + let date = NaiveDate::from_ymd_opt(current_date.year() + 1, 1, 1).unwrap(); + NaiveDateTime::new(date, NaiveTime::default()) } - // 1st of January in the next year - // SAFETY: This should be safe to unwrap as it will always create a valid date unless - // we are at the limit of dates that are representable. - Self::Annual => Date::from_calendar_date(current_date.year() + 1, Month::January, 1).unwrap(), } } @@ -41,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.duration + value.start; while current_date < end_date { let start_of_next_period = self.start_of_next_period(¤t_date); @@ -54,7 +61,7 @@ impl AggregationFrequency { sub_values.push(PeriodValue { start: current_date, - duration: current_duration, + duration: current_duration.into(), value: value.value, }); @@ -76,15 +83,15 @@ pub enum AggregationFunction { impl AggregationFunction { fn calc(&self, values: &[PeriodValue]) -> Option { match self { - AggregationFunction::Sum => Some(values.iter().map(|v| v.value * v.duration.whole_days() as f64).sum()), + AggregationFunction::Sum => Some(values.iter().map(|v| v.value * v.duration.fractional_days()).sum()), AggregationFunction::Mean => { - let ndays: i64 = values.iter().map(|v| v.duration.whole_days()).sum(); - if ndays == 0 { + let ndays: f64 = values.iter().map(|v| v.duration.fractional_days()).sum(); + if ndays == 0.0 { None } else { - let sum: f64 = values.iter().map(|v| v.value * v.duration.whole_days() as f64).sum(); + let sum: f64 = values.iter().map(|v| v.value * v.duration.fractional_days()).sum(); - Some(sum / ndays as f64) + Some(sum / ndays) } } AggregationFunction::Min => values.iter().map(|v| v.value).min_by(|a, b| { @@ -128,9 +135,9 @@ impl PeriodicAggregatorState { // New value is part of a different period (assume the next one). // Calculate the aggregated value of the previous period. - let agg_period = if let Some(agg_value) = agg_func.calc(¤t_values) { + let agg_period = if let Some(agg_value) = agg_func.calc(current_values) { let agg_duration = value.start - current_period_start; - Some(PeriodValue::new(current_period_start, agg_duration, agg_value)) + Some(PeriodValue::new(current_period_start, agg_duration.into(), agg_value)) } else { None }; @@ -160,7 +167,7 @@ impl PeriodicAggregatorState { fn calc_aggregation(&self, agg_func: &AggregationFunction) -> Option { if let Some(current_values) = &self.current_values { - if let Some(agg_value) = agg_func.calc(¤t_values) { + if let Some(agg_value) = agg_func.calc(current_values) { // SAFETY: The current_values vector is guaranteed to contain at least one value. let current_period_start = current_values .first() @@ -174,7 +181,7 @@ impl PeriodicAggregatorState { let current_period_duration = current_period_end - current_period_start; Some(PeriodValue::new( current_period_start, - current_period_duration, + current_period_duration.into(), agg_value, )) } else { @@ -194,13 +201,13 @@ struct PeriodicAggregator { #[derive(Debug, Copy, Clone)] pub struct PeriodValue { - pub start: Date, - pub duration: Duration, + pub start: NaiveDateTime, + pub duration: PywrDuration, pub value: f64, } impl PeriodValue { - pub fn new(start: Date, duration: Duration, value: f64) -> Self { + pub fn new(start: NaiveDateTime, duration: PywrDuration, value: f64) -> Self { Self { start, duration, value } } } @@ -319,9 +326,8 @@ impl Aggregator { mod tests { use super::{AggregationFrequency, AggregationFunction, Aggregator, PeriodicAggregator, PeriodicAggregatorState}; use crate::recorders::aggregator::PeriodValue; + use chrono::{Datelike, NaiveDate, TimeDelta}; use float_cmp::assert_approx_eq; - use time::macros::date; - use time::Duration; #[test] fn test_periodic_aggregator() { @@ -332,28 +338,32 @@ mod tests { let mut state = PeriodicAggregatorState::default(); - let agg_value = agg.process_value( - &mut state, - PeriodValue::new(date!(2023 - 01 - 30), Duration::days(1), 1.0), - ); + let start = NaiveDate::from_ymd_opt(2023, 1, 30) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let agg_value = agg.process_value(&mut state, PeriodValue::new(start, TimeDelta::days(1).into(), 1.0)); assert!(agg_value.is_none()); - let agg_value = agg.process_value( - &mut state, - PeriodValue::new(date!(2023 - 01 - 31), Duration::days(1), 1.0), - ); + let start = NaiveDate::from_ymd_opt(2023, 1, 31) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let agg_value = agg.process_value(&mut state, PeriodValue::new(start, TimeDelta::days(1).into(), 1.0)); assert!(agg_value.is_none()); - let agg_value = agg.process_value( - &mut state, - PeriodValue::new(date!(2023 - 02 - 01), Duration::days(1), 1.0), - ); + let start = NaiveDate::from_ymd_opt(2023, 2, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let agg_value = agg.process_value(&mut state, PeriodValue::new(start, TimeDelta::days(1).into(), 1.0)); assert!(agg_value.is_some()); - let agg_value = agg.process_value( - &mut state, - PeriodValue::new(date!(2023 - 02 - 02), Duration::days(1), 1.0), - ); + let start = NaiveDate::from_ymd_opt(2023, 2, 2) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let agg_value = agg.process_value(&mut state, PeriodValue::new(start, TimeDelta::days(1).into(), 1.0)); assert!(agg_value.is_none()); } @@ -380,11 +390,14 @@ mod tests { let mut state = max_annual_min.default_state(); - let mut date = date!(2023 - 01 - 01); + let mut date = NaiveDate::from_ymd_opt(2023, 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); for _i in 0..365 * 3 { - let value = PeriodValue::new(date, Duration::days(1), date.year() as f64); + let value = PeriodValue::new(date, TimeDelta::days(1).into(), date.year() as f64); let _agg_value = max_annual_min.append_value(&mut state, value); - date = date + Duration::days(1); + date = date + TimeDelta::days(1); } let final_value = max_annual_min.finalise(&mut state); @@ -395,4 +408,41 @@ mod tests { panic!("Final value is None!") } } + + #[test] + fn test_sub_daily_aggregation() { + let values = vec![ + PeriodValue::new( + NaiveDate::from_ymd_opt(2023, 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(), + TimeDelta::hours(1).into(), + 2.0, + ), + PeriodValue::new( + NaiveDate::from_ymd_opt(2023, 1, 1) + .unwrap() + .and_hms_opt(1, 0, 0) + .unwrap(), + TimeDelta::hours(2).into(), + 1.0, + ), + PeriodValue::new( + NaiveDate::from_ymd_opt(2023, 1, 1) + .unwrap() + .and_hms_opt(3, 0, 0) + .unwrap(), + TimeDelta::hours(1).into(), + 3.0, + ), + ]; + + let agg_value = AggregationFunction::Mean.calc(values.as_slice()).unwrap(); + assert_approx_eq!(f64, agg_value, 7.0 / 4.0); + + let agg_value = AggregationFunction::Sum.calc(values.as_slice()).unwrap(); + let expected = 2.0 * (1.0 / 24.0) + 1.0 * (2.0 / 24.0) + 3.0 * (1.0 / 24.0); + assert_approx_eq!(f64, agg_value, expected); + } } diff --git a/pywr-core/src/recorders/csv.rs b/pywr-core/src/recorders/csv.rs index 667d492c..82c24645 100644 --- a/pywr-core/src/recorders/csv.rs +++ b/pywr-core/src/recorders/csv.rs @@ -160,7 +160,7 @@ impl Recorder for CSVRecorder { .map_err(|e| PywrError::CSVError(e.to_string()))?; // There could be no scenario groups defined - if header_scenario_groups.len() > 0 { + if header_scenario_groups.is_empty() { for group in header_scenario_groups { writer .write_record(group) @@ -196,7 +196,7 @@ impl Recorder for CSVRecorder { for ms_scenario_states in metric_set_states.iter() { let metric_set_state = ms_scenario_states .get(*self.metric_set_idx.deref()) - .ok_or_else(|| PywrError::MetricSetIndexNotFound(self.metric_set_idx))?; + .ok_or(PywrError::MetricSetIndexNotFound(self.metric_set_idx))?; if let Some(current_values) = metric_set_state.current_values() { let values = current_values diff --git a/pywr-core/src/recorders/hdf.rs b/pywr-core/src/recorders/hdf.rs index f897dd0d..602229ae 100644 --- a/pywr-core/src/recorders/hdf.rs +++ b/pywr-core/src/recorders/hdf.rs @@ -5,6 +5,7 @@ use crate::network::Network; use crate::recorders::MetricSetIndex; use crate::scenario::ScenarioIndex; use crate::state::State; +use chrono::{Datelike, Timelike}; use hdf5::{Extents, Group}; use ndarray::{s, Array1}; use std::any::Any; @@ -26,20 +27,26 @@ struct Internal { #[derive(hdf5::H5Type, Copy, Clone, Debug)] #[repr(C)] -pub struct Date { +pub struct DateTime { index: usize, year: i32, month: u8, day: u8, + hour: u8, + minute: u8, + second: u8, } -impl Date { +impl DateTime { fn from_timestamp(ts: &Timestep) -> Self { Self { index: ts.index, year: ts.date.year(), - month: ts.date.month().into(), - day: ts.date.day(), + month: ts.date.month() as u8, + day: ts.date.day() as u8, + hour: ts.date.time().hour() as u8, + minute: ts.date.time().minute() as u8, + second: ts.date.time().second() as u8, } } } @@ -66,7 +73,7 @@ impl Recorder for HDF5Recorder { let mut datasets = Vec::new(); // Create the time table - let dates: Array1<_> = domain.time().timesteps().iter().map(Date::from_timestamp).collect(); + let dates: Array1<_> = domain.time().timesteps().iter().map(DateTime::from_timestamp).collect(); if let Err(e) = file.deref().new_dataset_builder().with_data(&dates).create("time") { return Err(PywrError::HDF5Error(e.to_string())); } diff --git a/pywr-core/src/scenario.rs b/pywr-core/src/scenario.rs index de8e43d7..698cac12 100644 --- a/pywr-core/src/scenario.rs +++ b/pywr-core/src/scenario.rs @@ -35,18 +35,19 @@ impl ScenarioGroupCollection { Self { groups } } - /// Number of [`ScenarioGroup`]s in the collection. pub fn len(&self) -> usize { self.groups.len() } + pub fn is_empty(&self) -> bool { + self.groups.is_empty() + } + /// Find a [`ScenarioGroup`]s index in the collection by name /// Find a `ScenarioGroup` in the collection by its index pub fn get_group(&self, idx: usize) -> Result<&ScenarioGroup, PywrError> { - self.groups - .get(idx) - .ok_or_else(|| PywrError::ScenarioGroupIndexNotFound(idx)) + self.groups.get(idx).ok_or(PywrError::ScenarioGroupIndexNotFound(idx)) } /// Get all `ScenarioGroup`s in the collection @@ -119,6 +120,11 @@ impl ScenarioDomain { pub fn len(&self) -> usize { self.scenario_indices.len() } + + pub fn is_empty(&self) -> bool { + self.scenario_indices.is_empty() + } + pub fn indices(&self) -> &[ScenarioIndex] { &self.scenario_indices } @@ -137,7 +143,7 @@ impl ScenarioDomain { impl From for ScenarioDomain { fn from(value: ScenarioGroupCollection) -> Self { // Handle creating at-least one scenario if the collection is empty. - if value.len() > 0 { + if !value.is_empty() { let scenario_group_names = value.groups.iter().map(|g| g.name.clone()).collect(); Self { diff --git a/pywr-core/src/solvers/builder.rs b/pywr-core/src/solvers/builder.rs index 4950de88..e644076e 100644 --- a/pywr-core/src/solvers/builder.rs +++ b/pywr-core/src/solvers/builder.rs @@ -405,17 +405,17 @@ where let agg_node = network.get_aggregated_node(agg_node_id)?; // Only create row for nodes that have factors if let Some(node_pairs) = agg_node.get_norm_factor_pairs(network, state) { - for ((n0, f0), (n1, f1)) in node_pairs { + for node_pair in node_pairs { // Modify the constraint matrix coefficients for the nodes // TODO error handling? let nodes = network.nodes(); - let node0 = nodes.get(&n0).expect("Node index not found!"); - let node1 = nodes.get(&n1).expect("Node index not found!"); + let node0 = nodes.get(&node_pair.node0.index).expect("Node index not found!"); + let node1 = nodes.get(&node_pair.node1.index).expect("Node index not found!"); self.builder .update_row_coefficients(*row_id, node0, 1.0, &self.col_edge_map); self.builder - .update_row_coefficients(*row_id, node1, -f0 / f1, &self.col_edge_map); + .update_row_coefficients(*row_id, node1, -node_pair.ratio(), &self.col_edge_map); self.builder.apply_row_bounds(row_id.to_usize().unwrap(), 0.0, 0.0); } diff --git a/pywr-core/src/solvers/clp/mod.rs b/pywr-core/src/solvers/clp/mod.rs index 23ca58c7..414c9dd1 100644 --- a/pywr-core/src/solvers/clp/mod.rs +++ b/pywr-core/src/solvers/clp/mod.rs @@ -92,7 +92,7 @@ impl ClpSimplex { pub fn modify_coefficient(&mut self, row: c_int, column: c_int, new_element: c_double) { unsafe { - Clp_modifyCoefficient(self.ptr, row, column, new_element, 1); + Clp_modifyCoefficient(self.ptr, row, column, new_element, true); } } diff --git a/pywr-core/src/solvers/clp/settings.rs b/pywr-core/src/solvers/clp/settings.rs index 7c2551aa..a8edf0ed 100644 --- a/pywr-core/src/solvers/clp/settings.rs +++ b/pywr-core/src/solvers/clp/settings.rs @@ -49,20 +49,12 @@ impl ClpSolverSettings { /// let settings = builder.build(); /// /// ``` +#[derive(Default)] pub struct ClpSolverSettingsBuilder { parallel: bool, threads: usize, } -impl Default for ClpSolverSettingsBuilder { - fn default() -> Self { - Self { - parallel: false, - threads: 0, - } - } -} - impl ClpSolverSettingsBuilder { pub fn parallel(&mut self) -> &mut Self { self.parallel = true; diff --git a/pywr-core/src/state.rs b/pywr-core/src/state.rs index 1ecafb6a..6f896b6e 100644 --- a/pywr-core/src/state.rs +++ b/pywr-core/src/state.rs @@ -271,6 +271,10 @@ impl ParameterStates { } } + pub fn get_value_state(&self, index: ParameterIndex) -> Option<&Option>> { + self.values.get(*index.deref()) + } + pub fn get_mut_value_state(&mut self, index: ParameterIndex) -> Option<&mut Option>> { self.values.get_mut(*index.deref()) } @@ -607,7 +611,15 @@ impl NetworkState { } } -/// State of the model simulation +/// State of the model simulation. +/// +/// This struct contains the state of the model simulation at a given point in time. The state +/// contains the current state of the network, the values of the parameters, the values of the +/// derived metrics, and the values of the inter-network transfers. +/// +/// This struct can be constructed using the [`StateBuilder`] and then updated using the various +/// methods to set the values of the parameters, derived metrics, and inter-network transfers. +/// #[derive(Debug, Clone)] pub struct State { network: NetworkState, @@ -617,24 +629,6 @@ pub struct State { } impl State { - pub fn new( - initial_node_states: Vec, - num_edges: usize, - initial_virtual_storage_states: Vec, - num_parameter_values: usize, - num_parameter_indices: usize, - num_multi_parameters: usize, - num_derived_metrics: usize, - num_inter_network_values: usize, - ) -> Self { - Self { - network: NetworkState::new(initial_node_states, num_edges, initial_virtual_storage_states), - parameters: ParameterValues::new(num_parameter_values, num_parameter_indices, num_multi_parameters), - derived_metrics: vec![0.0; num_derived_metrics], - inter_network_values: vec![0.0; num_inter_network_values], - } - } - pub fn get_network_state(&self) -> &NetworkState { &self.network } @@ -742,3 +736,95 @@ impl State { } } } + +/// Builder for the [`State`] struct. +/// +/// This builder is used to create a new state with the desired initial values. The builder +/// allows for the creation of a state with a specific number of nodes and edges, and optionally +/// with initial virtual storage, parameter, derived metric, and inter-network transfer states. +pub struct StateBuilder { + initial_node_states: Vec, + num_edges: usize, + initial_virtual_storage_states: Option>, + num_value_parameters: Option, + num_index_parameters: Option, + num_multi_parameters: Option, + num_derived_metrics: Option, + num_inter_network_values: Option, +} + +impl StateBuilder { + /// Create a new state builder with the desired initial node states and number of edges. + /// + /// # Arguments + /// + /// * `initial_node_states` - The initial states for the nodes in the network. + /// * `num_edges` - The number of edges in the network. + pub fn new(initial_node_states: Vec, num_edges: usize) -> Self { + Self { + initial_node_states, + num_edges, + initial_virtual_storage_states: None, + num_value_parameters: None, + num_index_parameters: None, + num_multi_parameters: None, + num_derived_metrics: None, + num_inter_network_values: None, + } + } + + /// Add initial virtual storage states to the builder. + pub fn with_virtual_storage_states(mut self, initial_virtual_storage_states: Vec) -> Self { + self.initial_virtual_storage_states = Some(initial_virtual_storage_states); + self + } + + /// Add the number of value parameters to the builder. + pub fn with_value_parameters(mut self, num_value_parameters: usize) -> Self { + self.num_value_parameters = Some(num_value_parameters); + self + } + + /// Add the number of index parameters to the builder. + pub fn with_index_parameters(mut self, num_index_parameters: usize) -> Self { + self.num_index_parameters = Some(num_index_parameters); + + self + } + + /// Add the number of multivalued parameters to the builder. + pub fn with_multi_parameters(mut self, num_multi_parameters: usize) -> Self { + self.num_multi_parameters = Some(num_multi_parameters); + self + } + + /// Add the number of derived metrics to the builder. + pub fn with_derived_metrics(mut self, num_derived_metrics: usize) -> Self { + self.num_derived_metrics = Some(num_derived_metrics); + self + } + + /// Add the number of inter-network transfer values to the builder. + pub fn with_inter_network_transfers(mut self, num_inter_network_values: usize) -> Self { + self.num_inter_network_values = Some(num_inter_network_values); + self + } + + /// Build the [`State`] from the builder. + pub fn build(self) -> State { + State { + network: NetworkState::new( + self.initial_node_states, + self.num_edges, + self.initial_virtual_storage_states.unwrap_or_default(), + ), + parameters: ParameterValues::new( + self.num_value_parameters.unwrap_or(0), + self.num_index_parameters.unwrap_or(0), + self.num_multi_parameters.unwrap_or(0), + ), + derived_metrics: vec![0.0; self.num_derived_metrics.unwrap_or(0)], + inter_network_values: vec![0.0; self.num_inter_network_values.unwrap_or(0)], + } + } +} diff --git a/pywr-core/src/test_utils.rs b/pywr-core/src/test_utils.rs index 4acc0a01..284825e4 100644 --- a/pywr-core/src/test_utils.rs +++ b/pywr-core/src/test_utils.rs @@ -14,20 +14,39 @@ use crate::solvers::ClpSolver; use crate::solvers::HighsSolver; #[cfg(feature = "ipm-simd")] use crate::solvers::SimdIpmF64Solver; -use crate::timestep::{TimeDomain, Timestepper}; +use crate::timestep::{TimeDomain, TimestepDuration, Timestepper}; use crate::PywrError; +use chrono::{Days, NaiveDate}; +use float_cmp::{approx_eq, F64Margin}; use ndarray::{Array, Array2}; use rand::Rng; use rand_distr::{Distribution, Normal}; -use time::ext::NumericalDuration; -use time::macros::date; pub fn default_timestepper() -> Timestepper { - Timestepper::new(date!(2020 - 01 - 01), date!(2020 - 01 - 15), 1) + let start = NaiveDate::from_ymd_opt(2020, 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let end = NaiveDate::from_ymd_opt(2020, 1, 15) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let duration = TimestepDuration::Days(1); + Timestepper::new(start, end, duration) } pub fn default_time_domain() -> TimeDomain { - default_timestepper().into() + default_timestepper().try_into().unwrap() +} + +pub fn default_domain() -> ModelDomain { + default_time_domain().into() +} + +pub fn default_model() -> Model { + let domain = default_domain(); + let network = Network::default(); + Model::new(domain, network) } /// Create a simple test network with three nodes. @@ -54,7 +73,7 @@ pub fn simple_network(network: &mut Network, inflow_scenario_index: usize, num_i let base_demand = 10.0; - let demand_factor = ConstantParameter::new("demand-factor", 1.2, None); + let demand_factor = ConstantParameter::new("demand-factor", 1.2); let demand_factor = network.add_parameter(Box::new(demand_factor)).unwrap(); let total_demand = AggregatedParameter::new( @@ -64,7 +83,7 @@ pub fn simple_network(network: &mut Network, inflow_scenario_index: usize, num_i ); let total_demand = network.add_parameter(Box::new(total_demand)).unwrap(); - let demand_cost = ConstantParameter::new("demand-cost", -10.0, None); + let demand_cost = ConstantParameter::new("demand-cost", -10.0); let demand_cost = network.add_parameter(Box::new(demand_cost)).unwrap(); let output_node = network.get_mut_node_by_name("output", None).unwrap(); @@ -81,7 +100,7 @@ pub fn simple_model(num_scenarios: usize) -> Model { let mut scenario_collection = ScenarioGroupCollection::default(); scenario_collection.add_group("test-scenario", num_scenarios); - let domain = ModelDomain::from(default_timestepper(), scenario_collection); + let domain = ModelDomain::from(default_timestepper(), scenario_collection).unwrap(); let mut network = Network::default(); let idx = domain @@ -112,10 +131,10 @@ pub fn simple_storage_model() -> Model { // Apply demand to the model // TODO convenience function for adding a constant constraint. - let demand = ConstantParameter::new("demand", 10.0, None); + let demand = ConstantParameter::new("demand", 10.0); let demand = network.add_parameter(Box::new(demand)).unwrap(); - let demand_cost = ConstantParameter::new("demand-cost", -10.0, None); + let demand_cost = ConstantParameter::new("demand-cost", -10.0); let demand_cost = network.add_parameter(Box::new(demand_cost)).unwrap(); let output_node = network.get_mut_node_by_name("output", None).unwrap(); @@ -146,8 +165,13 @@ pub fn run_and_assert_parameter( ) { let p_idx = model.network_mut().add_parameter(parameter).unwrap(); - let start = date!(2020 - 01 - 01); - let _end = start.checked_add((expected_values.nrows() as i64 - 1).days()).unwrap(); + let start = NaiveDate::from_ymd_opt(2020, 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let _end = start + .checked_add_days(Days::new(expected_values.nrows() as u64 - 1)) + .unwrap(); let rec = AssertionRecorder::new("assert", Metric::ParameterValue(p_idx), expected_values, ulps, epsilon); @@ -286,12 +310,21 @@ pub fn make_random_model( num_scenarios: usize, rng: &mut R, ) -> Result { - let timestepper = Timestepper::new(date!(2020 - 01 - 01), date!(2020 - 04 - 09), 1); + let start = NaiveDate::from_ymd_opt(2020, 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let end = NaiveDate::from_ymd_opt(2020, 4, 9) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let duration = TimestepDuration::Days(1); + let timestepper = Timestepper::new(start, end, duration); let mut scenario_collection = ScenarioGroupCollection::default(); scenario_collection.add_group("test-scenario", num_scenarios); - let domain = ModelDomain::from(timestepper, scenario_collection); + let domain = ModelDomain::from(timestepper, scenario_collection).unwrap(); let inflow_scenario_group_index = domain .scenarios() @@ -344,3 +377,20 @@ mod tests { .expect("Failed to run model!"); } } + +/// Compare two arrays of f64 +pub fn assert_approx_array_eq(calculated_values: &[f64], expected_values: &[f64]) { + let margins = F64Margin { + epsilon: 2.0, + ulps: (f64::EPSILON * 2.0) as i64, + }; + for (i, (calculated, expected)) in calculated_values.iter().zip(expected_values).enumerate() { + if !approx_eq!(f64, *calculated, *expected, margins) { + panic!( + r#"assertion failed on item #{i:?} + actual: `{calculated:?}`, + expected: `{expected:?}`"#, + ) + } + } +} diff --git a/pywr-core/src/timestep.rs b/pywr-core/src/timestep.rs index ce2988d9..48c71e38 100644 --- a/pywr-core/src/timestep.rs +++ b/pywr-core/src/timestep.rs @@ -1,19 +1,81 @@ +use chrono::{Months, NaiveDateTime, TimeDelta}; +use polars::datatypes::TimeUnit; +use polars::time::ClosedWindow; use pyo3::prelude::*; use std::ops::Add; -use time::{Date, Duration}; + +use crate::PywrError; + +const SECS_IN_DAY: i64 = 60 * 60 * 24; + +/// A newtype for `chrono::TimeDelta` that provides a couple of useful convenience methods. +#[pyclass] +#[derive(Debug, Copy, Clone)] +pub struct PywrDuration(TimeDelta); + +impl From for PywrDuration { + fn from(duration: TimeDelta) -> Self { + Self(duration) + } +} + +impl PartialEq for PywrDuration { + fn eq(&self, other: &TimeDelta) -> bool { + self.0 == *other + } +} + +impl PartialEq for PywrDuration { + fn eq(&self, other: &PywrDuration) -> bool { + self.0 == other.0 + } +} + +impl Add for PywrDuration { + type Output = NaiveDateTime; + + fn add(self, datetime: NaiveDateTime) -> NaiveDateTime { + datetime + self.0 + } +} + +impl PywrDuration { + /// Create a new `PywrDuration` from a number of days. + pub fn days(days: i64) -> Self { + Self(TimeDelta::days(days)) + } + + /// Returns the number of whole days in the duration, if the total duration is a whole number of days. + pub fn whole_days(&self) -> Option { + if self.0.num_seconds() % SECS_IN_DAY == 0 { + Some(self.0.num_days()) + } else { + None + } + } + + // Returns the fractional number of days in the duration. + pub fn fractional_days(&self) -> f64 { + self.0.num_seconds() as f64 / SECS_IN_DAY as f64 + } + + pub fn whole_nanoseconds(&self) -> Option { + self.0.num_nanoseconds() + } +} type TimestepIndex = usize; #[pyclass] #[derive(Debug, Copy, Clone)] pub struct Timestep { - pub date: Date, + pub date: NaiveDateTime, pub index: TimestepIndex, - pub duration: Duration, + pub duration: PywrDuration, } impl Timestep { - pub fn new(date: Date, index: TimestepIndex, duration: Duration) -> Self { + pub fn new(date: NaiveDateTime, index: TimestepIndex, duration: PywrDuration) -> Self { Self { date, index, duration } } @@ -21,59 +83,111 @@ impl Timestep { self.index == 0 } - // pub fn parse_from_str(date: &str, fmt: &str, index: TimestepIndex, timestep: i64) -> Result { - // Ok(Self { - // date: Date::parse_from_str(date, fmt)?, - // index, - // duration: Duration::days(timestep), - // }) - // } - pub(crate) fn days(&self) -> f64 { - self.duration.as_seconds_f64() / 3600.0 / 24.0 + self.duration.fractional_days() } } -impl Add for Timestep { +impl Add for Timestep { type Output = Timestep; - fn add(self, other: Duration) -> Self { + fn add(self, other: PywrDuration) -> Self { Self { - date: self.date + other, + date: self.date + other.0, index: self.index + 1, duration: other, } } } +#[derive(Debug)] +pub enum TimestepDuration { + Days(i64), + Frequency(String), +} + #[derive(Debug)] pub struct Timestepper { - start: Date, - end: Date, - timestep: Duration, + start: NaiveDateTime, + end: NaiveDateTime, + timestep: TimestepDuration, } impl Timestepper { - pub fn new(start: Date, end: Date, timestep: i64) -> Self { - Self { - start, - end, - timestep: Duration::days(timestep), - } + pub fn new(start: NaiveDateTime, end: NaiveDateTime, timestep: TimestepDuration) -> Self { + Self { start, end, timestep } } /// Create a vector of `Timestep`s between the start and end dates at the given duration. - fn timesteps(&self) -> Vec { + fn timesteps(&self) -> Result, PywrError> { + match &self.timestep { + TimestepDuration::Days(days) => Ok(self.generate_timesteps_from_days(*days)), + TimestepDuration::Frequency(frequency) => self.generate_timesteps_from_frequency(frequency.as_str()), + } + } + + /// Creates a vector of `Timestep`s between the start and end dates at the given duration of days. + fn generate_timesteps_from_days(&self, days: i64) -> Vec { let mut timesteps: Vec = Vec::new(); - let mut current = Timestep::new(self.start, 0, self.timestep); + let duration = PywrDuration::days(days); + let mut current = Timestep::new(self.start, 0, duration); while current.date <= self.end { - let next = current + self.timestep; + let next = current + duration; timesteps.push(current); current = next; } timesteps } + + /// Creates a vector of `Timestep`s between the start and end dates for a given frequency `&str`. + /// + /// Valid frequency strings are those that can be parsed by `polars::time::Duration::parse`. See: [https://docs.rs/polars-time/latest/polars_time/struct.Duration.html#method.parse] + fn generate_timesteps_from_frequency(&self, frequency: &str) -> Result, PywrError> { + let duration = polars::time::Duration::parse(frequency); + + // Need to add an extra day to the end date so that the duration of the last timestep can be calculated. + let end = if duration.days_only() { + self.end + TimeDelta::days(duration.days()) + } else if duration.weeks_only() { + self.end + TimeDelta::weeks(duration.weeks()) + } else if duration.months_only() { + let months = Months::new(duration.months() as u32); + self.end + months + } else { + let months = Months::new(duration.months() as u32); + self.end + + months + + TimeDelta::days(duration.days()) + + TimeDelta::weeks(duration.weeks()) + + TimeDelta::nanoseconds(duration.nanoseconds()) + }; + + let dates = polars::time::date_range( + "timesteps", + self.start, + end, + duration, + ClosedWindow::Both, + TimeUnit::Milliseconds, + None, + ) + .map_err(|e| PywrError::TimestepRangeGenerationError(e.to_string()))? + .as_datetime_iter() + .map(|x| x.ok_or(PywrError::TimestepGenerationError(frequency.to_string()))) + .collect::, PywrError>>()?; + + let timesteps = dates + .windows(2) + .enumerate() + .map(|(i, dates)| { + let duration = dates[1] - dates[0]; + Timestep::new(dates[0], i, duration.into()) + }) + .collect::>(); + + Ok(timesteps) + } } /// The time domain that a model will be simulated over. @@ -84,7 +198,7 @@ pub struct TimeDomain { impl TimeDomain { /// Return the duration of each time-step. - pub fn step_duration(&self) -> Duration { + pub fn step_duration(&self) -> PywrDuration { // This relies on the assumption that all time-steps are the same length. // Ideally, this invariant would be refactored to have the duration stored here in `TimeDomain`, // rather than in `Timestep`. @@ -107,12 +221,114 @@ impl TimeDomain { pub fn last_timestep(&self) -> &Timestep { self.timesteps.last().expect("No time-steps defined.") } + + pub fn is_empty(&self) -> bool { + self.timesteps.is_empty() + } } -impl From for TimeDomain { - fn from(value: Timestepper) -> Self { - Self { - timesteps: value.timesteps(), - } +impl TryFrom for TimeDomain { + type Error = PywrError; + + fn try_from(value: Timestepper) -> Result { + let timesteps = value.timesteps()?; + Ok(Self { timesteps }) + } +} + +#[cfg(test)] +mod test { + use chrono::{NaiveDateTime, TimeDelta}; + + use crate::timestep::{PywrDuration, SECS_IN_DAY}; + + use super::{TimestepDuration, Timestepper}; + + #[test] + fn test_days() { + let start = NaiveDateTime::parse_from_str("2021-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let end = NaiveDateTime::parse_from_str("2021-01-10 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let timestep = TimestepDuration::Days(1); + + let timestepper = Timestepper::new(start, end, timestep); + let timesteps = timestepper.timesteps().unwrap(); + assert!(timesteps.len() == 10); + assert_eq!(timesteps.first().unwrap().duration, TimeDelta::days(1)); + assert_eq!(timesteps.last().unwrap().duration, TimeDelta::days(1)); + + let timestep = TimestepDuration::Frequency(String::from("1d")); + + let timestepper = Timestepper::new(start, end, timestep); + let timesteps = timestepper.timesteps().unwrap(); + assert!(timesteps.len() == 10); + assert_eq!(timesteps.first().unwrap().duration, TimeDelta::days(1)); + assert_eq!(timesteps.last().unwrap().duration, TimeDelta::days(1)); + } + + #[test] + fn test_weeks() { + let start = NaiveDateTime::parse_from_str("2021-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let end = NaiveDateTime::parse_from_str("2021-01-22 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let timestep = TimestepDuration::Frequency(String::from("1w")); + + let timestepper = Timestepper::new(start, end, timestep); + let timesteps = timestepper.timesteps().unwrap(); + + assert!(timesteps.len() == 4); + assert_eq!(timesteps.first().unwrap().duration, TimeDelta::days(7)); + assert_eq!(timesteps.last().unwrap().duration, TimeDelta::days(7)); + } + + #[test] + fn test_months() { + let start = NaiveDateTime::parse_from_str("2021-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let end = NaiveDateTime::parse_from_str("2021-04-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let timestep = TimestepDuration::Frequency(String::from("1mo")); + + let timestepper = Timestepper::new(start, end, timestep); + let timesteps = timestepper.timesteps().unwrap(); + assert!(timesteps.len() == 4); + assert_eq!(timesteps[0].duration, TimeDelta::days(31)); + assert_eq!(timesteps[1].duration, TimeDelta::days(28)); + assert_eq!(timesteps[2].duration, TimeDelta::days(31)); + assert_eq!(timesteps[3].duration, TimeDelta::days(30)); + } + + #[test] + fn test_hours() { + let start = NaiveDateTime::parse_from_str("2021-01-01 12:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let end = NaiveDateTime::parse_from_str("2021-01-01 16:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let timestep = TimestepDuration::Frequency(String::from("1h")); + + let timestepper = Timestepper::new(start, end, timestep); + let timesteps = timestepper.timesteps().unwrap(); + assert!(timesteps.len() == 5); + assert_eq!(timesteps.first().unwrap().duration, TimeDelta::hours(1)); + assert_eq!(timesteps.last().unwrap().duration, TimeDelta::hours(1)); + } + + #[test] + fn test_pywr_duration() { + let duration = PywrDuration::days(5); + assert_eq!(duration.whole_days(), Some(5)); + assert_eq!(duration.fractional_days(), 5.0); + + let duration: PywrDuration = TimeDelta::hours(12).into(); + assert_eq!(duration.whole_days(), None); + assert_eq!(duration.fractional_days(), 0.5); + + let duration: PywrDuration = TimeDelta::minutes(30).into(); + assert_eq!(duration.whole_days(), None); + assert_eq!(duration.fractional_days(), 1.0 / 48.0); + + let duration_secs = SECS_IN_DAY + 1; + let duration: PywrDuration = TimeDelta::seconds(duration_secs).into(); + assert_eq!(duration.whole_days(), None); + assert_eq!(duration.fractional_days(), duration_secs as f64 / SECS_IN_DAY as f64); + + let duration_secs = SECS_IN_DAY - 1; + let duration: PywrDuration = TimeDelta::seconds(duration_secs).into(); + assert_eq!(duration.whole_days(), None); + assert_eq!(duration.fractional_days(), duration_secs as f64 / SECS_IN_DAY as f64); } } diff --git a/pywr-core/src/virtual_storage.rs b/pywr-core/src/virtual_storage.rs index 3abeeeea..96786b44 100644 --- a/pywr-core/src/virtual_storage.rs +++ b/pywr-core/src/virtual_storage.rs @@ -3,11 +3,11 @@ use crate::node::{ConstraintValue, FlowConstraints, NodeMeta, StorageConstraints use crate::state::{State, VirtualStorageState}; use crate::timestep::Timestep; use crate::{NodeIndex, PywrError}; +use chrono::{Datelike, Month, NaiveDateTime}; use std::fmt; use std::fmt::{Display, Formatter}; use std::num::NonZeroUsize; use std::ops::{Deref, DerefMut}; -use time::{Date, Month}; #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub struct VirtualStorageIndex(usize); @@ -88,7 +88,7 @@ impl VirtualStorageVec { pub enum VirtualStorageReset { Never, - DayOfYear { day: u8, month: Month }, + DayOfYear { day: u32, month: Month }, NumberOfMonths { months: i32 }, } @@ -175,7 +175,7 @@ impl VirtualStorage { match self.reset { VirtualStorageReset::Never => false, VirtualStorageReset::DayOfYear { day, month } => { - (timestep.date.day() == day) && (timestep.date.month() == month) + (timestep.date.day() == day) && (timestep.date.month() == month.number_from_month()) } VirtualStorageReset::NumberOfMonths { months } => { // Get the date when the virtual storage was last reset @@ -244,7 +244,7 @@ impl VirtualStorage { } /// Calculate the number of months between `current` [Timestep] and the `last_reset` [Timestep]. -fn months_since_last_reset(current: &Date, last_reset: &Date) -> i32 { +fn months_since_last_reset(current: &NaiveDateTime, last_reset: &NaiveDateTime) -> i32 { (current.year() - last_reset.year()) * 12 + current.month() as i32 - last_reset.month() as i32 } @@ -259,29 +259,52 @@ mod tests { use crate::test_utils::{default_timestepper, run_all_solvers, simple_model}; use crate::timestep::Timestep; use crate::virtual_storage::{months_since_last_reset, VirtualStorageReset}; + use chrono::NaiveDate; use ndarray::Array; use std::num::NonZeroUsize; - use time::macros::date; /// Test the calculation of number of months since last reset #[test] fn test_months_since_last_reset() { - assert_eq!( - months_since_last_reset(&date!(2022 - 12 - 31), &date!(2022 - 12 - 31)), - 0 - ); - assert_eq!( - months_since_last_reset(&date!(2023 - 12 - 31), &date!(2022 - 12 - 31)), - 12 - ); - assert_eq!( - months_since_last_reset(&date!(2023 - 01 - 1), &date!(2022 - 12 - 31)), - 1 - ); - assert_eq!( - months_since_last_reset(&date!(2022 - 12 - 1), &date!(2022 - 12 - 31)), - 0 - ); + let current = NaiveDate::from_ymd_opt(2022, 12, 31) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let last_reset = NaiveDate::from_ymd_opt(2022, 12, 31) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + assert_eq!(months_since_last_reset(¤t, &last_reset), 0); + + let current = NaiveDate::from_ymd_opt(2023, 12, 31) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let last_reset = NaiveDate::from_ymd_opt(2022, 12, 31) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + assert_eq!(months_since_last_reset(¤t, &last_reset), 12); + + let current = NaiveDate::from_ymd_opt(2023, 01, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let last_reset = NaiveDate::from_ymd_opt(2022, 12, 31) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + assert_eq!(months_since_last_reset(¤t, &last_reset), 1); + + let current = NaiveDate::from_ymd_opt(2022, 12, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let last_reset = NaiveDate::from_ymd_opt(2022, 12, 31) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + assert_eq!(months_since_last_reset(¤t, &last_reset), 0); } /// Test the virtual storage constraints @@ -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 = default_timestepper().try_into().unwrap(); + let model = Model::new(domain, network); // Test all solvers run_all_solvers(&model); } diff --git a/pywr-python/Cargo.toml b/pywr-python/Cargo.toml index 06b92d37..6ff9c8d8 100644 --- a/pywr-python/Cargo.toml +++ b/pywr-python/Cargo.toml @@ -14,18 +14,16 @@ categories = ["science", "simulation"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -pyo3 = { workspace = true } +pyo3 = { workspace = true, features = ["extension-module", "macros"] } pyo3-polars = { workspace = true } +pyo3-log = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -time = { workspace = true, features = ["serde", "serde-well-known", "serde-human-readable", "macros"] } +chrono = { workspace = true } pywr-core = { path="../pywr-core" } pywr-schema = { path="../pywr-schema" } -[features] -extension-module = ["pyo3/extension-module"] -default = ["extension-module"] [lib] name = "pywr" diff --git a/pywr-python/src/lib.rs b/pywr-python/src/lib.rs index 0ed1c73a..5f73a5c5 100644 --- a/pywr-python/src/lib.rs +++ b/pywr-python/src/lib.rs @@ -1,6 +1,7 @@ +use chrono::NaiveDate; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; -use pyo3::types::{PyDate, PyDateAccess, PyDict, PyType}; +use pyo3::types::{PyDateAccess, PyDateTime, PyDict, PyTimeAccess, PyType}; /// Python API /// /// The following structures provide a Python API to access the core model structures. @@ -13,9 +14,10 @@ use pywr_core::solvers::{ClIpmF32Solver, ClIpmF64Solver, ClIpmSolverSettings}; use pywr_core::solvers::{ClpSolver, ClpSolverSettings, ClpSolverSettingsBuilder}; #[cfg(feature = "highs")] use pywr_core::solvers::{HighsSolver, HighsSolverSettings, HighsSolverSettings, HighsSolverSettingsBuilde}; +use pywr_schema::model::DateType; use std::fmt; use std::path::PathBuf; -use time::Date; +use std::str::FromStr; #[derive(Debug)] struct PySchemaError { @@ -61,11 +63,25 @@ pub struct Schema { #[pymethods] impl Schema { #[new] - fn new(title: &str, start: &PyDate, end: &PyDate) -> Self { + fn new(title: &str, start: &PyDateTime, end: &PyDateTime) -> Self { // SAFETY: We know that the date & month are valid because it is a Python date. - 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::DateTime( + NaiveDate::from_ymd_opt(start.get_year(), start.get_month() as u32, start.get_day() as u32) + .unwrap() + .and_hms_opt( + start.get_hour() as u32, + start.get_minute() as u32, + start.get_second() as u32, + ) + .unwrap(), + ); + + let end = DateType::DateTime( + NaiveDate::from_ymd_opt(end.get_year(), end.get_month() as u32, end.get_day() as u32) + .unwrap() + .and_hms_opt(end.get_hour() as u32, end.get_minute() as u32, end.get_second() as u32) + .unwrap(), + ); Self { schema: pywr_schema::PywrModel::new(title, &start, &end), @@ -196,6 +212,8 @@ fn build_highs_settings(kwargs: Option<&PyDict>) -> PyResult PyResult<()> { + pyo3_log::init(); + m.add_class::()?; m.add_class::()?; diff --git a/pywr-schema/Cargo.toml b/pywr-schema/Cargo.toml index e71152f1..6634ccfe 100644 --- a/pywr-schema/Cargo.toml +++ b/pywr-schema/Cargo.toml @@ -30,9 +30,8 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } pywr-v1-schema = { workspace = true } -time = { workspace = true, features = ["serde", "serde-well-known", "serde-human-readable", "macros"] } pywr-core = { path="../pywr-core" } -chrono = "0.4.33" +chrono = { workspace = true, features = ["serde"] } [dev-dependencies] tempfile = "3.3.0" diff --git a/pywr-schema/src/error.rs b/pywr-schema/src/error.rs index 35da8488..616c629f 100644 --- a/pywr-schema/src/error.rs +++ b/pywr-schema/src/error.rs @@ -49,12 +49,6 @@ pub enum SchemaError { UnsupportedFileFormat, #[error("Python error: {0}")] PythonError(String), - #[error("invalid date format description")] - InvalidDateFormatDescription(#[from] time::error::InvalidFormatDescription), - #[error("failed to parse date")] - DateParse(#[from] time::error::Parse), - #[error("invalid date component range")] - InvalidDateComponentRange(#[from] time::error::ComponentRange), #[error("hdf5 error: {0}")] HDF5Error(String), #[error("csv error: {0}")] @@ -71,6 +65,8 @@ pub enum SchemaError { InterNetworkTransferNotFound(String), #[error("Invalid rolling window definition on parameter {name}. Must convert to a positive integer.")] InvalidRollingWindow { name: String }, + #[error("Failed to load parameter {name}: {error}")] + LoadParameter { name: String, error: String }, } impl From for PyErr { @@ -116,4 +112,8 @@ pub enum ConversionError { expected: String, actual: String, }, + #[error("'{0}' could not be parsed into a NaiveDate")] + UnparseableDate(String), + #[error("Chrono out of range error: {0}")] + OutOfRange(#[from] chrono::OutOfRange), } diff --git a/pywr-schema/src/model.rs b/pywr-schema/src/model.rs index 12e122f2..a2bd881a 100644 --- a/pywr-schema/src/model.rs +++ b/pywr-schema/src/model.rs @@ -7,10 +7,12 @@ use crate::metric_sets::MetricSet; use crate::outputs::Output; use crate::parameters::{MetricFloatReference, TryIntoV2Parameter}; use crate::timeseries::{LoadedTimeseriesCollection, Timeseries}; +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; use pywr_core::models::ModelDomain; +use pywr_core::timestep::TimestepDuration; use pywr_core::PywrError; use std::path::{Path, PathBuf}; -use time::Date; +use std::str::FromStr; #[derive(serde::Deserialize, serde::Serialize, Clone)] pub struct Metadata { @@ -33,7 +35,7 @@ impl TryFrom for Metadata { } } -#[derive(serde::Deserialize, serde::Serialize, Clone)] +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] #[serde(untagged)] pub enum Timestep { Days(i64), @@ -49,10 +51,17 @@ impl From for Timestep { } } -#[derive(serde::Deserialize, serde::Serialize, Clone)] +#[derive(serde::Deserialize, serde::Serialize, Clone, Copy, Debug)] +#[serde(untagged)] +pub enum DateType { + Date(NaiveDate), + DateTime(NaiveDateTime), +} + +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)] pub struct Timestepper { - pub start: Date, - pub end: Date, + pub start: DateType, + pub end: DateType, pub timestep: Timestep, } @@ -60,9 +69,19 @@ impl TryFrom for Timestepper { type Error = ConversionError; fn try_from(v1: pywr_v1_schema::model::Timestepper) -> Result { + let start = DateType::Date( + NaiveDate::from_ymd_opt(v1.start.year(), v1.start.month() as u32, v1.start.day() as u32) + .ok_or(ConversionError::UnparseableDate(v1.start.to_string()))?, + ); + + let end = DateType::Date( + NaiveDate::from_ymd_opt(v1.end.year(), v1.end.month() as u32, v1.end.day() as u32) + .ok_or(ConversionError::UnparseableDate(v1.end.to_string()))?, + ); + Ok(Self { - start: v1.start, - end: v1.end, + start, + end, timestep: v1.timestep.into(), }) } @@ -71,11 +90,21 @@ impl TryFrom for Timestepper { impl From for pywr_core::timestep::Timestepper { fn from(ts: Timestepper) -> Self { let timestep = match ts.timestep { - Timestep::Days(d) => d, - _ => todo!(), + Timestep::Days(d) => TimestepDuration::Days(d), + Timestep::Frequency(f) => TimestepDuration::Frequency(f), + }; + + let start = match ts.start { + DateType::Date(date) => NaiveDateTime::new(date, NaiveTime::default()), + DateType::DateTime(date_time) => date_time, + }; + + let end = match ts.end { + DateType::Date(date) => NaiveDateTime::new(date, NaiveTime::default()), + DateType::DateTime(date_time) => date_time, }; - Self::new(ts.start, ts.end, timestep) + Self::new(start, end, timestep) } } @@ -97,16 +126,20 @@ pub struct PywrNetwork { pub outputs: Option>, } +impl FromStr for PywrNetwork { + type Err = SchemaError; + + fn from_str(s: &str) -> Result { + Ok(serde_json::from_str(s)?) + } +} + impl PywrNetwork { pub fn from_path>(path: P) -> Result { let data = std::fs::read_to_string(path).map_err(|e| SchemaError::IO(e.to_string()))?; Ok(serde_json::from_str(data.as_str())?) } - pub fn from_str(data: &str) -> Result { - Ok(serde_json::from_str(data)?) - } - pub fn get_node_by_name(&self, name: &str) -> Option<&Node> { self.nodes.iter().find(|n| n.name() == name) } @@ -154,7 +187,7 @@ impl PywrNetwork { if let Err(e) = node.add_to_model( &mut network, &self, - &domain, + domain, &tables, data_path, inter_network_transfers, @@ -212,8 +245,8 @@ impl PywrNetwork { for parameter in remaining_parameters.into_iter() { if let Err(e) = parameter.add_to_model( &mut network, - &self, - &domain, + self, + domain, &tables, data_path, inter_network_transfers, @@ -246,8 +279,8 @@ impl PywrNetwork { for node in &self.nodes { node.set_constraints( &mut network, - &self, - &domain, + self, + domain, &tables, data_path, inter_network_transfers, @@ -309,8 +342,16 @@ pub struct PywrModel { pub network: PywrNetwork, } +impl FromStr for PywrModel { + type Err = SchemaError; + + fn from_str(s: &str) -> Result { + Ok(serde_json::from_str(s)?) + } +} + impl PywrModel { - pub fn new(title: &str, start: &Date, end: &Date) -> Self { + pub fn new(title: &str, start: &DateType, end: &DateType) -> Self { Self { metadata: Metadata { title: title.to_string(), @@ -332,10 +373,6 @@ impl PywrModel { Ok(serde_json::from_str(data.as_str())?) } - pub fn from_str(data: &str) -> Result { - Ok(serde_json::from_str(data)?) - } - pub fn build_model( &self, data_path: Option<&Path>, @@ -351,7 +388,7 @@ impl PywrModel { } } - let domain = ModelDomain::from(timestepper, scenario_collection); + let domain = ModelDomain::from(timestepper, scenario_collection)?; let network = self.network.build_network(&domain, data_path, output_path, &[])?; @@ -496,16 +533,20 @@ pub struct PywrMultiNetworkModel { pub networks: Vec, } +impl FromStr for PywrMultiNetworkModel { + type Err = SchemaError; + + fn from_str(s: &str) -> Result { + Ok(serde_json::from_str(s)?) + } +} + impl PywrMultiNetworkModel { pub fn from_path>(path: P) -> Result { let data = std::fs::read_to_string(path).map_err(|e| SchemaError::IO(e.to_string()))?; Ok(serde_json::from_str(data.as_str())?) } - pub fn from_str(data: &str) -> Result { - Ok(serde_json::from_str(data)?) - } - pub fn build_model( &self, data_path: Option<&Path>, @@ -521,7 +562,7 @@ impl PywrMultiNetworkModel { } } - let domain = ModelDomain::from(timestepper, scenario_collection); + let domain = ModelDomain::from(timestepper, scenario_collection)?; let mut model = pywr_core::models::MultiNetworkModel::new(domain); let mut schemas = Vec::with_capacity(self.networks.len()); @@ -588,6 +629,7 @@ impl PywrMultiNetworkModel { #[cfg(test)] mod tests { use super::{PywrModel, PywrMultiNetworkModel}; + use crate::model::Timestepper; use crate::parameters::{ AggFunc, AggregatedParameter, ConstantParameter, ConstantValue, DynamicFloatValue, MetricFloatReference, MetricFloatValue, Parameter, ParameterMeta, @@ -672,7 +714,6 @@ mod tests { comment: None, }, value: ConstantValue::Literal(10.0), - variable: None, }), Parameter::Aggregated(AggregatedParameter { meta: ParameterMeta { @@ -729,7 +770,6 @@ mod tests { comment: None, }, value: ConstantValue::Literal(10.0), - variable: None, }), Parameter::Constant(ConstantParameter { meta: ParameterMeta { @@ -737,7 +777,6 @@ mod tests { comment: None, }, value: ConstantValue::Literal(10.0), - variable: None, }), ]); } @@ -846,4 +885,70 @@ mod tests { model.run::(&Default::default()).unwrap(); } + + #[test] + fn test_date() { + let timestepper_str = r#" + { + "start": "2015-01-01", + "end": "2015-12-31", + "timestep": 1 + } + "#; + + let timestep: Timestepper = serde_json::from_str(timestepper_str).unwrap(); + + match timestep.start { + super::DateType::Date(date) => { + assert_eq!(date, chrono::NaiveDate::from_ymd_opt(2015, 1, 1).unwrap()); + } + _ => panic!("Expected a date"), + } + + match timestep.end { + super::DateType::Date(date) => { + assert_eq!(date, chrono::NaiveDate::from_ymd_opt(2015, 12, 31).unwrap()); + } + _ => panic!("Expected a date"), + } + } + + #[test] + fn test_datetime() { + let timestepper_str = r#" + { + "start": "2015-01-01T12:30:00", + "end": "2015-01-01T14:30:00", + "timestep": 1 + } + "#; + + let timestep: Timestepper = serde_json::from_str(timestepper_str).unwrap(); + + match timestep.start { + super::DateType::DateTime(date_time) => { + assert_eq!( + date_time, + chrono::NaiveDate::from_ymd_opt(2015, 1, 1) + .unwrap() + .and_hms_opt(12, 30, 0) + .unwrap() + ); + } + _ => panic!("Expected a date"), + } + + match timestep.end { + super::DateType::DateTime(date_time) => { + assert_eq!( + date_time, + chrono::NaiveDate::from_ymd_opt(2015, 1, 1) + .unwrap() + .and_hms_opt(14, 30, 0) + .unwrap() + ); + } + _ => panic!("Expected a date"), + } + } } diff --git a/pywr-schema/src/nodes/annual_virtual_storage.rs b/pywr-schema/src/nodes/annual_virtual_storage.rs index 1e60b5f6..63c4fc68 100644 --- a/pywr-schema/src/nodes/annual_virtual_storage.rs +++ b/pywr-schema/src/nodes/annual_virtual_storage.rs @@ -16,7 +16,7 @@ use std::path::Path; #[derive(serde::Deserialize, serde::Serialize, Clone)] pub struct AnnualReset { pub day: u8, - pub month: time::Month, + pub month: chrono::Month, pub use_initial_volume: bool, } @@ -24,7 +24,7 @@ impl Default for AnnualReset { fn default() -> Self { Self { day: 1, - month: time::Month::January, + month: chrono::Month::January, use_initial_volume: false, } } @@ -108,7 +108,7 @@ impl AnnualVirtualStorageNode { .collect::, _>>()?; let reset = VirtualStorageReset::DayOfYear { - day: self.reset.day, + day: self.reset.day as u32, month: self.reset.month, }; @@ -197,6 +197,8 @@ impl TryFrom for AnnualVirtualStorageNode { }); }; + let month = chrono::Month::try_from(v1.reset_month as u8)?; + let n = Self { meta, nodes: v1.nodes, @@ -207,7 +209,7 @@ impl TryFrom for AnnualVirtualStorageNode { initial_volume, reset: AnnualReset { day: v1.reset_day, - month: v1.reset_month, + month, use_initial_volume: v1.reset_to_initial_volume, }, }; diff --git a/pywr-schema/src/nodes/core.rs b/pywr-schema/src/nodes/core.rs index 33b72aca..a4843a55 100644 --- a/pywr-schema/src/nodes/core.rs +++ b/pywr-schema/src/nodes/core.rs @@ -469,9 +469,9 @@ impl Default for StorageInitialVolume { } } -impl Into for StorageInitialVolume { - fn into(self) -> CoreStorageInitialVolume { - match self { +impl From for CoreStorageInitialVolume { + fn from(v: StorageInitialVolume) -> Self { + match v { StorageInitialVolume::Absolute(v) => CoreStorageInitialVolume::Absolute(v), StorageInitialVolume::Proportional(v) => CoreStorageInitialVolume::Proportional(v), } diff --git a/pywr-schema/src/nodes/loss_link.rs b/pywr-schema/src/nodes/loss_link.rs index 1a3b8718..2c11ce5b 100644 --- a/pywr-schema/src/nodes/loss_link.rs +++ b/pywr-schema/src/nodes/loss_link.rs @@ -143,11 +143,11 @@ impl LossLinkNode { } } NodeAttribute::Outflow => { - let idx = network.get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name().as_deref())?; + let idx = network.get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name())?; Metric::NodeOutFlow(idx) } NodeAttribute::Loss => { - let idx = network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name().as_deref())?; + let idx = network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name())?; // This is an output node that only supports inflow Metric::NodeInFlow(idx) } diff --git a/pywr-schema/src/nodes/mod.rs b/pywr-schema/src/nodes/mod.rs index 1ba68f87..d9908235 100644 --- a/pywr-schema/src/nodes/mod.rs +++ b/pywr-schema/src/nodes/mod.rs @@ -121,7 +121,7 @@ impl NodeBuilder { pub fn next_default_name_for_model(mut self, network: &PywrNetwork) -> Self { let mut num = 1; loop { - let name = format!("{}-{}", self.ty.to_string(), num); + let name = format!("{}-{}", self.ty, num); if network.get_node_by_name(&name).is_none() { // No node with this name found! self.name = Some(name); diff --git a/pywr-schema/src/nodes/piecewise_link.rs b/pywr-schema/src/nodes/piecewise_link.rs index 17149714..ea244fd9 100644 --- a/pywr-schema/src/nodes/piecewise_link.rs +++ b/pywr-schema/src/nodes/piecewise_link.rs @@ -193,7 +193,7 @@ impl TryFrom for PiecewiseLinkNode { let steps = costs .into_iter() - .zip(max_flows.into_iter()) + .zip(max_flows) .map(|(cost, max_flow)| PiecewiseLinkStep { max_flow, min_flow: None, diff --git a/pywr-schema/src/nodes/river_split_with_gauge.rs b/pywr-schema/src/nodes/river_split_with_gauge.rs index 9ac72260..26fd9aaf 100644 --- a/pywr-schema/src/nodes/river_split_with_gauge.rs +++ b/pywr-schema/src/nodes/river_split_with_gauge.rs @@ -198,7 +198,7 @@ impl RiverSplitWithGaugeNode { .map(|(i, _)| network.get_node_index_by_name(self.meta.name.as_str(), Self::split_sub_name(i).as_deref())) .collect::>()?; - indices.extend(split_idx.into_iter()); + indices.extend(split_idx); let metric = match attr { NodeAttribute::Inflow => Metric::MultiNodeInFlow { diff --git a/pywr-schema/src/nodes/rolling_virtual_storage.rs b/pywr-schema/src/nodes/rolling_virtual_storage.rs index 6d4a4112..889bedfa 100644 --- a/pywr-schema/src/nodes/rolling_virtual_storage.rs +++ b/pywr-schema/src/nodes/rolling_virtual_storage.rs @@ -36,7 +36,13 @@ impl RollingWindow { pub fn as_timesteps(&self, time: &TimeDomain) -> Option { match self { Self::Days(days) => { - let timesteps = days.get() / time.step_duration().whole_days() as usize; + let ts_days = match time.step_duration().whole_days() { + Some(d) => d as usize, + // If the timestep duration is not a whole number of days then the rolling window cannot be specified in days. + None => return None, + }; + + let timesteps = days.get() / ts_days; NonZeroUsize::new(timesteps) } diff --git a/pywr-schema/src/nodes/water_treatment_works.rs b/pywr-schema/src/nodes/water_treatment_works.rs index 83d647f8..db542a72 100644 --- a/pywr-schema/src/nodes/water_treatment_works.rs +++ b/pywr-schema/src/nodes/water_treatment_works.rs @@ -265,11 +265,11 @@ impl WaterTreatmentWorks { } } NodeAttribute::Outflow => { - let idx = network.get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name().as_deref())?; + let idx = network.get_node_index_by_name(self.meta.name.as_str(), Self::net_sub_name())?; Metric::NodeOutFlow(idx) } NodeAttribute::Loss => { - let idx = network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name().as_deref())?; + let idx = network.get_node_index_by_name(self.meta.name.as_str(), Self::loss_sub_name())?; // This is an output node that only supports inflow Metric::NodeInFlow(idx) } diff --git a/pywr-schema/src/outputs/csv.rs b/pywr-schema/src/outputs/csv.rs index 305b2d6a..221f1d40 100644 --- a/pywr-schema/src/outputs/csv.rs +++ b/pywr-schema/src/outputs/csv.rs @@ -33,6 +33,7 @@ impl CsvOutput { mod tests { use crate::PywrModel; use pywr_core::solvers::{ClpSolver, ClpSolverSettings}; + use std::str::FromStr; use tempfile::TempDir; fn csv1_str() -> &'static str { diff --git a/pywr-schema/src/outputs/hdf.rs b/pywr-schema/src/outputs/hdf.rs index 4cccc664..4e02dc0e 100644 --- a/pywr-schema/src/outputs/hdf.rs +++ b/pywr-schema/src/outputs/hdf.rs @@ -35,6 +35,7 @@ impl Hdf5Output { mod tests { use crate::PywrModel; use pywr_core::solvers::{ClpSolver, ClpSolverSettings}; + use std::str::FromStr; use tempfile::TempDir; fn model_str() -> &'static str { diff --git a/pywr-schema/src/parameters/core.rs b/pywr-schema/src/parameters/core.rs index aa034685..4e316928 100644 --- a/pywr-schema/src/parameters/core.rs +++ b/pywr-schema/src/parameters/core.rs @@ -95,20 +95,24 @@ pub enum ActivationFunction { Logistic { growth_rate: f64, max: f64 }, } -impl Into for ActivationFunction { - fn into(self) -> pywr_core::parameters::ActivationFunction { - match self { - Self::Unit { min, max } => pywr_core::parameters::ActivationFunction::Unit { min, max }, - Self::Rectifier { min, max, off_value } => pywr_core::parameters::ActivationFunction::Rectifier { - min, - max, - neg_value: off_value.unwrap_or(0.0), - }, - Self::BinaryStep { on_value, off_value } => pywr_core::parameters::ActivationFunction::BinaryStep { - pos_value: on_value, - neg_value: off_value.unwrap_or(0.0), - }, - Self::Logistic { growth_rate, max } => { +impl From for pywr_core::parameters::ActivationFunction { + fn from(a: ActivationFunction) -> Self { + match a { + ActivationFunction::Unit { min, max } => pywr_core::parameters::ActivationFunction::Unit { min, max }, + ActivationFunction::Rectifier { min, max, off_value } => { + pywr_core::parameters::ActivationFunction::Rectifier { + min, + max, + neg_value: off_value.unwrap_or(0.0), + } + } + ActivationFunction::BinaryStep { on_value, off_value } => { + pywr_core::parameters::ActivationFunction::BinaryStep { + pos_value: on_value, + neg_value: off_value.unwrap_or(0.0), + } + } + ActivationFunction::Logistic { growth_rate, max } => { pywr_core::parameters::ActivationFunction::Logistic { growth_rate, max } } } @@ -152,8 +156,6 @@ pub struct ConstantParameter { /// In the simple case this will be the value used by the network. However, if an activation /// function is specified this value will be the `x` value for that activation function. pub value: ConstantValue, - /// Definition of optional variable settings. - pub variable: Option, } impl ConstantParameter { @@ -170,19 +172,7 @@ impl ConstantParameter { network: &mut pywr_core::network::Network, tables: &LoadedTableCollection, ) -> Result { - let variable = match &self.variable { - None => None, - Some(v) => { - // Only set the variable data if the user has indicated the variable is active. - if v.is_active { - Some(v.activation.into()) - } else { - None - } - } - }; - - let p = pywr_core::parameters::ConstantParameter::new(&self.meta.name, self.value.load(tables)?, variable); + let p = pywr_core::parameters::ConstantParameter::new(&self.meta.name, self.value.load(tables)?); Ok(network.add_parameter(Box::new(p))?) } } @@ -206,7 +196,6 @@ impl TryFromV1Parameter for ConstantParameter { let p = Self { meta: v1.meta.into_v2_parameter(parent_node, unnamed_count), value, - variable: None, // TODO implement conversion of v1 variable definition }; Ok(p) } diff --git a/pywr-schema/src/parameters/interpolated.rs b/pywr-schema/src/parameters/interpolated.rs index c9c4b5cb..6b6129b8 100644 --- a/pywr-schema/src/parameters/interpolated.rs +++ b/pywr-schema/src/parameters/interpolated.rs @@ -110,11 +110,7 @@ impl InterpolatedParameter { }) .collect::, _>>()?; - let points = xp - .into_iter() - .zip(fp.into_iter()) - .map(|(xp, fp)| (xp, fp)) - .collect::>(); + let points = xp.into_iter().zip(fp).collect::>(); let p = pywr_core::parameters::InterpolatedParameter::new( &self.meta.name, diff --git a/pywr-schema/src/parameters/mod.rs b/pywr-schema/src/parameters/mod.rs index b2daac0a..27e7cd8a 100644 --- a/pywr-schema/src/parameters/mod.rs +++ b/pywr-schema/src/parameters/mod.rs @@ -39,7 +39,7 @@ pub use super::parameters::indexed_array::IndexedArrayParameter; pub use super::parameters::polynomial::Polynomial1DParameter; pub use super::parameters::profiles::{ DailyProfileParameter, MonthlyProfileParameter, RadialBasisFunction, RbfProfileParameter, - RbfProfileVariableSettings, UniformDrawdownProfileParameter, + RbfProfileVariableSettings, UniformDrawdownProfileParameter, WeeklyProfileParameter, }; pub use super::parameters::python::PythonParameter; pub use super::parameters::tables::TablesArrayParameter; @@ -154,6 +154,7 @@ pub enum Parameter { DailyProfile(DailyProfileParameter), IndexedArray(IndexedArrayParameter), MonthlyProfile(MonthlyProfileParameter), + WeeklyProfile(WeeklyProfileParameter), UniformDrawdownProfile(UniformDrawdownProfileParameter), Max(MaxParameter), Min(MinParameter), @@ -185,6 +186,7 @@ impl Parameter { Self::DailyProfile(p) => p.meta.name.as_str(), Self::IndexedArray(p) => p.meta.name.as_str(), Self::MonthlyProfile(p) => p.meta.name.as_str(), + Self::WeeklyProfile(p) => p.meta.name.as_str(), Self::UniformDrawdownProfile(p) => p.meta.name.as_str(), Self::Max(p) => p.meta.name.as_str(), Self::Min(p) => p.meta.name.as_str(), @@ -216,6 +218,7 @@ impl Parameter { Self::DailyProfile(_) => "DailyProfile", Self::IndexedArray(_) => "IndexedArray", Self::MonthlyProfile(_) => "MonthlyProfile", + Self::WeeklyProfile(_) => "WeeklyProfile", Self::UniformDrawdownProfile(_) => "UniformDrawdownProfile", Self::Max(_) => "Max", Self::Min(_) => "Min", @@ -320,6 +323,7 @@ impl Parameter { timeseries, )?), Self::MonthlyProfile(p) => ParameterType::Parameter(p.add_to_model(network, tables)?), + Self::WeeklyProfile(p) => ParameterType::Parameter(p.add_to_model(network, tables)?), Self::UniformDrawdownProfile(p) => ParameterType::Parameter(p.add_to_model(network, tables)?), Self::Max(p) => ParameterType::Parameter(p.add_to_model( network, @@ -485,7 +489,7 @@ impl TryFromV1Parameter for Parameter { CoreParameter::Deficit(p) => { return Err(ConversionError::DeprecatedParameter { ty: "DeficitParameter".to_string(), - name: p.meta.map(|m| m.name).flatten().unwrap_or("unnamed".to_string()), + name: p.meta.and_then(|m| m.name).unwrap_or("unnamed".to_string()), instead: "Use a derived metric instead.".to_string(), }) } @@ -498,11 +502,16 @@ impl TryFromV1Parameter for Parameter { CoreParameter::InterpolatedFlow(p) => { Parameter::Interpolated(p.try_into_v2_parameter(parent_node, unnamed_count)?) } + CoreParameter::NegativeMax(_) => todo!("Implement NegativeMaxParameter"), + CoreParameter::NegativeMin(_) => todo!("Implement NegativeMinParameter"), CoreParameter::HydropowerTarget(_) => todo!("Implement HydropowerTargetParameter"), + CoreParameter::WeeklyProfile(p) => { + Parameter::WeeklyProfile(p.try_into_v2_parameter(parent_node, unnamed_count)?) + } CoreParameter::Storage(p) => { return Err(ConversionError::DeprecatedParameter { ty: "StorageParameter".to_string(), - name: p.meta.map(|m| m.name).flatten().unwrap_or("unnamed".to_string()), + name: p.meta.and_then(|m| m.name).unwrap_or("unnamed".to_string()), instead: "Use a derived metric instead.".to_string(), }) } @@ -511,7 +520,7 @@ impl TryFromV1Parameter for Parameter { CoreParameter::Flow(p) => { return Err(ConversionError::DeprecatedParameter { ty: "FlowParameter".to_string(), - name: p.meta.map(|m| m.name).flatten().unwrap_or("unnamed".to_string()), + name: p.meta.and_then(|m| m.name).unwrap_or("unnamed".to_string()), instead: "Use a derived metric instead.".to_string(), }) } @@ -535,7 +544,6 @@ impl TryFromV1Parameter for Parameter { comment: Some(comment), }, value: ConstantValue::Literal(0.0), - variable: None, }) } }; @@ -702,7 +710,7 @@ impl MetricFloatValue { } Err(_) => { // An error retrieving a parameter with this name; assume it needs creating. - match definition.add_to_model(network, schema, &domain, tables, data_path, inter_network_transfers, timeseries)? { + match definition.add_to_model(network, schema, domain, tables, data_path, inter_network_transfers, timeseries)? { ParameterType::Parameter(idx) => Ok(Metric::ParameterValue(idx)), ParameterType::Index(_) => Err(SchemaError::UnexpectedParameterType(format!( "Found index parameter of type '{}' with name '{}' where an float parameter was expected.", diff --git a/pywr-schema/src/parameters/offset.rs b/pywr-schema/src/parameters/offset.rs index bd062168..e912342e 100644 --- a/pywr-schema/src/parameters/offset.rs +++ b/pywr-schema/src/parameters/offset.rs @@ -37,8 +37,6 @@ pub struct OffsetParameter { pub offset: ConstantValue, /// The metric from which to apply the offset. pub metric: DynamicFloatValue, - /// Definition of optional variable settings. - pub variable: Option, } impl OffsetParameter { @@ -60,18 +58,6 @@ impl OffsetParameter { inter_network_transfers: &[PywrMultiNetworkTransfer], timeseries: &LoadedTimeseriesCollection, ) -> Result { - let variable = match &self.variable { - None => None, - Some(v) => { - // Only set the variable data if the user has indicated the variable is active. - if v.is_active { - Some(v.activation.into()) - } else { - None - } - } - }; - let idx = self.metric.load( network, schema, @@ -82,7 +68,7 @@ impl OffsetParameter { timeseries, )?; - let p = pywr_core::parameters::OffsetParameter::new(&self.meta.name, idx, self.offset.load(tables)?, variable); + let p = pywr_core::parameters::OffsetParameter::new(&self.meta.name, idx, self.offset.load(tables)?); Ok(network.add_parameter(Box::new(p))?) } } diff --git a/pywr-schema/src/parameters/profiles.rs b/pywr-schema/src/parameters/profiles.rs index 8bb3dc7d..0a24b895 100644 --- a/pywr-schema/src/parameters/profiles.rs +++ b/pywr-schema/src/parameters/profiles.rs @@ -3,11 +3,12 @@ use crate::error::{ConversionError, SchemaError}; use crate::parameters::{ ConstantFloatVec, ConstantValue, DynamicFloatValueType, IntoV2Parameter, ParameterMeta, TryFromV1Parameter, }; -use pywr_core::parameters::ParameterIndex; +use pywr_core::parameters::{ParameterIndex, WeeklyProfileError, WeeklyProfileValues}; use pywr_v1_schema::parameters::{ DailyProfileParameter as DailyProfileParameterV1, MonthInterpDay as MonthInterpDayV1, MonthlyProfileParameter as MonthlyProfileParameterV1, RbfProfileParameter as RbfProfileParameterV1, UniformDrawdownProfileParameter as UniformDrawdownProfileParameterV1, + WeeklyProfileParameter as WeeklyProfileParameterV1, }; use std::collections::HashMap; @@ -176,12 +177,12 @@ impl UniformDrawdownProfileParameter { tables: &LoadedTableCollection, ) -> Result { let reset_day = match &self.reset_day { - Some(v) => v.load(tables)? as u8, + Some(v) => v.load(tables)? as u32, None => 1, }; let reset_month = match &self.reset_month { - Some(v) => time::Month::try_from(v.load(tables)? as u8)?, - None => time::Month::January, + Some(v) => v.load(tables)? as u32, + None => 1, }; let residual_days = match &self.residual_days { Some(v) => v.load(tables)? as u8, @@ -239,7 +240,7 @@ impl RadialBasisFunction { let rbf = match self { Self::Linear => pywr_core::parameters::RadialBasisFunction::Linear, Self::Cubic => pywr_core::parameters::RadialBasisFunction::Cubic, - Self::Quintic => pywr_core::parameters::RadialBasisFunction::Cubic, + Self::Quintic => pywr_core::parameters::RadialBasisFunction::Quintic, Self::ThinPlateSpline => pywr_core::parameters::RadialBasisFunction::ThinPlateSpline, Self::Gaussian { epsilon } => { let epsilon = match epsilon { @@ -314,12 +315,12 @@ pub struct RbfProfileVariableSettings { pub value_lower_bounds: Option, } -impl Into for RbfProfileVariableSettings { - fn into(self) -> pywr_core::parameters::RbfProfileVariableConfig { - pywr_core::parameters::RbfProfileVariableConfig::new( - self.days_of_year_range, - self.value_upper_bounds.unwrap_or(f64::INFINITY), - self.value_lower_bounds.unwrap_or(0.0), +impl From for pywr_core::parameters::RbfProfileVariableConfig { + fn from(settings: RbfProfileVariableSettings) -> Self { + Self::new( + settings.days_of_year_range, + settings.value_upper_bounds.unwrap_or(f64::INFINITY), + settings.value_lower_bounds.unwrap_or(0.0), ) } } @@ -352,8 +353,6 @@ pub struct RbfProfileParameter { pub points: Vec<(u32, f64)>, /// The distance function used for interpolation. pub function: RadialBasisFunction, - /// Definition of optional variable settings. - pub variable: Option, } impl RbfProfileParameter { @@ -365,22 +364,9 @@ impl RbfProfileParameter { } pub fn add_to_model(&self, network: &mut pywr_core::network::Network) -> Result { - let variable = match self.variable { - None => None, - Some(v) => { - // Only set the variable data if the user has indicated the variable is active. - if v.is_active { - Some(v.into()) - } else { - None - } - } - }; - let function = self.function.into_core_rbf(&self.points)?; - let p = - pywr_core::parameters::RbfProfileParameter::new(&self.meta.name, self.points.clone(), function, variable); + let p = pywr_core::parameters::RbfProfileParameter::new(&self.meta.name, self.points.clone(), function); Ok(network.add_parameter(Box::new(p))?) } } @@ -395,12 +381,7 @@ impl TryFromV1Parameter for RbfProfileParameter { ) -> Result { let meta: ParameterMeta = v1.meta.into_v2_parameter(parent_node, unnamed_count); - let points = v1 - .days_of_year - .into_iter() - .zip(v1.values.into_iter()) - .map(|(doy, v)| (doy, v)) - .collect(); + let points = v1.days_of_year.into_iter().zip(v1.values).collect(); if v1.rbf_kwargs.contains_key("smooth") { return Err(ConversionError::UnsupportedFeature { @@ -435,7 +416,7 @@ impl TryFromV1Parameter for RbfProfileParameter { let function = if let Some(function_value) = v1.rbf_kwargs.get("function") { if let Some(function_str) = function_value.as_str() { // Function kwarg is a string! - let f = match function_str { + match function_str { "multiquadric" => RadialBasisFunction::MultiQuadric { epsilon }, "inverse" => RadialBasisFunction::InverseMultiQuadric { epsilon }, "gaussian" => RadialBasisFunction::Gaussian { epsilon }, @@ -448,8 +429,7 @@ impl TryFromV1Parameter for RbfProfileParameter { name: meta.name.clone(), }) } - }; - f + } } else { return Err(ConversionError::UnexpectedType { attr: "function".to_string(), @@ -463,13 +443,153 @@ impl TryFromV1Parameter for RbfProfileParameter { RadialBasisFunction::MultiQuadric { epsilon } }; + let p = Self { meta, points, function }; + + Ok(p) + } +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Copy, Clone)] +pub enum WeeklyInterpDay { + First, + Last, +} + +impl From for pywr_core::parameters::WeeklyInterpDay { + fn from(value: WeeklyInterpDay) -> Self { + match value { + WeeklyInterpDay::First => Self::First, + WeeklyInterpDay::Last => Self::Last, + } + } +} + +/// A parameter to handle a weekly profile of 52 or 53 weeks. +/// +/// # Arguments +/// +/// * `values` - The weekly values; this can be an array of 52 or 53 values. With 52 items, +/// the value for the 53rd week (day 364 - 29th Dec or 30th +/// Dec for a leap year) is copied from week 52nd. +/// * `interp_day` - This is an optional field to control the parameter interpolation. When this +/// is not provided, the profile is piecewise. When this equals "First" or "Last", the values +/// are linearly interpolated in each week and the string specifies whether the given values are +/// the first or last day of the week. See the examples below for more information. +/// +/// ## Interpolation notes +/// When the profile is interpolated, the following assumptions are made for a 52-week profile due to the missing +/// values on the 53rd week: +/// - when `interp_day` is First, the upper boundary in the 52nd and 53rd week is the +/// same (i.e. the value on 1st January) +/// - when `interp_day` is Last the 1st and last week will share the same lower bound (i.e. the +/// value on the last week). +/// +/// This does apply to a 53-week profile. +/// +/// # Examples +/// ## Without interpolation +/// This defines a piece-wise weekly profile. Each day of the same week has the same value: +/// ```json +/// { +/// "type": "WeeklyProfile", +/// "values": [0.4, 4, ... , 12] +/// } +/// ``` +/// In the example above, the parameter returns `0.4` from 1st to 6th January +/// for week 1, `4` for the week 2 (7st to 13th) and so on. +/// +/// ## Interpolation +/// ### interp_day = "First" +/// ```json +/// { +/// "type": "WeeklyProfile", +/// "values": [0.4, 4, 9, ... , 10, 12], +/// "interp_day": "First" +/// } +/// ``` +/// This defines an interpolated profile where the values in the 1st week are derived by +/// linearly interpolating between `0.4` and `4`, in the 2nd week between `4` and `9`. +/// The values in the last week are interpolated between `12` and `0.4` (i.e the value on 1st +/// January). +/// +/// ### interp_day = "Last" +/// ```json +/// { +/// "type": "WeeklyProfile", +/// "values": [0.4, 4, 9, ... , 10, 12], +/// "interp_day": "Last" +/// } +/// ``` +/// This defines an interpolated profile where the values in the 1st week are derived by +/// linearly interpolating between `12` and `0.4`, in the 2nd week between `0.4` and `4`. +/// The values in the last week are interpolated between `10` and `12` (i.e the value on 31st +/// December). +/// +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct WeeklyProfileParameter { + #[serde(flatten)] + pub meta: ParameterMeta, + pub values: ConstantFloatVec, + pub interp_day: Option, +} + +impl WeeklyProfileParameter { + pub fn node_references(&self) -> HashMap<&str, &str> { + HashMap::new() + } + pub fn parameters(&self) -> HashMap<&str, DynamicFloatValueType> { + HashMap::new() + } + + pub fn add_to_model( + &self, + network: &mut pywr_core::network::Network, + tables: &LoadedTableCollection, + ) -> Result { + let p = pywr_core::parameters::WeeklyProfileParameter::new( + &self.meta.name, + WeeklyProfileValues::try_from(self.values.load(tables)?.as_slice()).map_err( + |err: WeeklyProfileError| SchemaError::LoadParameter { + name: self.meta.name.to_string(), + error: err.to_string(), + }, + )?, + self.interp_day.map(|id| id.into()), + ); + Ok(network.add_parameter(Box::new(p))?) + } +} + +impl TryFromV1Parameter for WeeklyProfileParameter { + type Error = ConversionError; + + fn try_from_v1_parameter( + v1: WeeklyProfileParameterV1, + parent_node: Option<&str>, + unnamed_count: &mut usize, + ) -> Result { + let meta: ParameterMeta = v1.meta.into_v2_parameter(parent_node, unnamed_count); + + let values: ConstantFloatVec = if let Some(values) = v1.values { + // pywr v1 only accept a 52-week profile + ConstantFloatVec::Literal(values) + } else if let Some(external) = v1.external { + ConstantFloatVec::External(external.try_into()?) + } else if let Some(table_ref) = v1.table_ref { + ConstantFloatVec::Table(table_ref.try_into()?) + } else { + return Err(ConversionError::MissingAttribute { + name: meta.name, + attrs: vec!["values".to_string(), "table".to_string(), "url".to_string()], + }); + }; + + // pywr 1 does not support interpolation let p = Self { meta, - points, - function, - variable: None, + values, + interp_day: None, }; - Ok(p) } } diff --git a/pywr-schema/src/timeseries/align_and_resample.rs b/pywr-schema/src/timeseries/align_and_resample.rs index edcb1d47..884a793b 100644 --- a/pywr-schema/src/timeseries/align_and_resample.rs +++ b/pywr-schema/src/timeseries/align_and_resample.rs @@ -43,7 +43,11 @@ pub fn align_and_resample( None => return Err(SchemaError::TimeseriesDurationNotFound(name.to_string())), }; - let model_duration = domain.time().step_duration().whole_nanoseconds() as i64; + let model_duration = domain + .time() + .step_duration() + .whole_nanoseconds() + .expect("Nano seconds could not be extracted from model step duration") as i64; let df = match model_duration.cmp(×eries_duration) { Ordering::Greater => { @@ -82,15 +86,13 @@ pub fn align_and_resample( } fn slice_start(df: DataFrame, time_col: &str, domain: &ModelDomain) -> Result { - let start = domain.time().first_timestep().date.midnight().assume_utc(); - let start = NaiveDateTime::from_timestamp_opt(start.unix_timestamp(), 0).unwrap(); + let start = domain.time().first_timestep().date; let df = df.clone().lazy().filter(col(time_col).gt_eq(lit(start))).collect()?; Ok(df) } fn slice_end(df: DataFrame, time_col: &str, domain: &ModelDomain) -> Result { - let end = domain.time().last_timestep().date.midnight().assume_utc(); - let end = NaiveDateTime::from_timestamp_opt(end.unix_timestamp(), 0).unwrap(); + let end = domain.time().last_timestep().date; let df = df.clone().lazy().filter(col(time_col).lt_eq(lit(end))).collect()?; Ok(df) } @@ -103,20 +105,18 @@ mod tests { use pywr_core::{ models::ModelDomain, scenario::{ScenarioDomain, ScenarioGroupCollection}, - timestep::{TimeDomain, Timestepper}, + timestep::{TimeDomain, TimestepDuration, Timestepper}, }; - use time::{Date, Month}; - use crate::timeseries::{align_and_resample::align_and_resample, tests}; + use crate::timeseries::align_and_resample::align_and_resample; #[test] fn test_downsample_and_slice() { - let time_domain: TimeDomain = Timestepper::new( - Date::from_calendar_date(2021, Month::January, 7).unwrap(), - Date::from_calendar_date(2021, Month::January, 20).unwrap(), - 7, - ) - .into(); + let start = NaiveDateTime::parse_from_str("2021-01-07 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let end = NaiveDateTime::parse_from_str("2021-01-20 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let timestep = TimestepDuration::Days(7); + let timestepper = Timestepper::new(start, end, timestep); + let time_domain = TimeDomain::try_from(timestepper).unwrap(); let scenario_domain: ScenarioDomain = ScenarioGroupCollection::new(vec![]).into(); @@ -167,12 +167,12 @@ mod tests { #[test] fn test_upsample_and_slice() { - let time_domain: TimeDomain = Timestepper::new( - Date::from_calendar_date(2021, Month::January, 1).unwrap(), - Date::from_calendar_date(2021, Month::January, 14).unwrap(), - 1, - ) - .into(); + let start = NaiveDateTime::parse_from_str("2021-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let end = NaiveDateTime::parse_from_str("2021-01-14 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let timestep = TimestepDuration::Days(1); + let timestepper = Timestepper::new(start, end, timestep); + let time_domain = TimeDomain::try_from(timestepper).unwrap(); + let scenario_domain: ScenarioDomain = ScenarioGroupCollection::new(vec![]).into(); let domain = ModelDomain::new(time_domain, scenario_domain); @@ -206,12 +206,12 @@ mod tests { #[test] fn test_no_resample_slice() { - let time_domain: TimeDomain = Timestepper::new( - Date::from_calendar_date(2021, Month::January, 1).unwrap(), - Date::from_calendar_date(2021, Month::January, 3).unwrap(), - 1, - ) - .into(); + let start = NaiveDateTime::parse_from_str("2021-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let end = NaiveDateTime::parse_from_str("2021-01-03 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(); + let timestep = TimestepDuration::Days(1); + let timestepper = Timestepper::new(start, end, timestep); + let time_domain = TimeDomain::try_from(timestepper).unwrap(); + let scenario_domain: ScenarioDomain = ScenarioGroupCollection::new(vec![]).into(); let domain = ModelDomain::new(time_domain, scenario_domain); diff --git a/pywr-schema/src/timeseries/mod.rs b/pywr-schema/src/timeseries/mod.rs index c3d4ffc0..83d8f5e0 100644 --- a/pywr-schema/src/timeseries/mod.rs +++ b/pywr-schema/src/timeseries/mod.rs @@ -107,9 +107,9 @@ impl LoadedTimeseriesCollection { mod tests { use std::path::PathBuf; + use chrono::{Datelike, NaiveDate}; use ndarray::Array; use pywr_core::{metric::Metric, recorders::AssertionRecorder, test_utils::run_all_solvers}; - use time::Date; use crate::PywrModel; @@ -130,7 +130,7 @@ mod tests { let mut model = schema.build_model(Some(model_dir.as_path()), None).unwrap(); let expected = Array::from_shape_fn((365, 1), |(x, _)| { - (Date::from_ordinal_date(2021, (x + 1) as u16).unwrap().day() + 2) as f64 + (NaiveDate::from_yo_opt(2021, (x + 1) as u32).unwrap().day() + 2) as f64 }); let idx = model.network().get_node_by_name("output1", None).unwrap().index();