fram-core 0.1.0a1__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {fram_core-0.1.0a1.dist-info → fram_core-0.1.1.dist-info}/METADATA +6 -5
  2. fram_core-0.1.1.dist-info/RECORD +100 -0
  3. {fram_core-0.1.0a1.dist-info → fram_core-0.1.1.dist-info}/WHEEL +1 -1
  4. framcore/Base.py +22 -3
  5. framcore/Model.py +26 -9
  6. framcore/__init__.py +2 -1
  7. framcore/aggregators/Aggregator.py +30 -11
  8. framcore/aggregators/HydroAggregator.py +37 -25
  9. framcore/aggregators/NodeAggregator.py +65 -30
  10. framcore/aggregators/WindSolarAggregator.py +22 -30
  11. framcore/attributes/Arrow.py +6 -4
  12. framcore/attributes/ElasticDemand.py +13 -13
  13. framcore/attributes/ReservoirCurve.py +3 -17
  14. framcore/attributes/SoftBound.py +2 -5
  15. framcore/attributes/StartUpCost.py +14 -3
  16. framcore/attributes/Storage.py +17 -5
  17. framcore/attributes/TargetBound.py +2 -4
  18. framcore/attributes/__init__.py +2 -4
  19. framcore/attributes/hydro/HydroBypass.py +9 -2
  20. framcore/attributes/hydro/HydroGenerator.py +24 -7
  21. framcore/attributes/hydro/HydroPump.py +32 -10
  22. framcore/attributes/hydro/HydroReservoir.py +4 -4
  23. framcore/attributes/level_profile_attributes.py +250 -53
  24. framcore/components/Component.py +27 -3
  25. framcore/components/Demand.py +18 -4
  26. framcore/components/Flow.py +26 -4
  27. framcore/components/HydroModule.py +45 -4
  28. framcore/components/Node.py +32 -9
  29. framcore/components/Thermal.py +12 -8
  30. framcore/components/Transmission.py +17 -2
  31. framcore/components/wind_solar.py +25 -10
  32. framcore/curves/LoadedCurve.py +0 -9
  33. framcore/expressions/Expr.py +137 -36
  34. framcore/expressions/__init__.py +3 -1
  35. framcore/expressions/_get_constant_from_expr.py +14 -20
  36. framcore/expressions/queries.py +121 -84
  37. framcore/expressions/units.py +30 -3
  38. framcore/fingerprints/fingerprint.py +0 -1
  39. framcore/juliamodels/JuliaModel.py +13 -3
  40. framcore/loaders/loaders.py +0 -2
  41. framcore/metadata/ExprMeta.py +13 -7
  42. framcore/metadata/LevelExprMeta.py +16 -1
  43. framcore/metadata/Member.py +7 -7
  44. framcore/metadata/__init__.py +1 -1
  45. framcore/querydbs/CacheDB.py +1 -1
  46. framcore/solvers/Solver.py +21 -6
  47. framcore/solvers/SolverConfig.py +4 -4
  48. framcore/timeindexes/AverageYearRange.py +9 -2
  49. framcore/timeindexes/ConstantTimeIndex.py +7 -2
  50. framcore/timeindexes/DailyIndex.py +14 -2
  51. framcore/timeindexes/FixedFrequencyTimeIndex.py +105 -53
  52. framcore/timeindexes/HourlyIndex.py +14 -2
  53. framcore/timeindexes/IsoCalendarDay.py +5 -3
  54. framcore/timeindexes/ListTimeIndex.py +103 -23
  55. framcore/timeindexes/ModelYear.py +8 -2
  56. framcore/timeindexes/ModelYears.py +11 -2
  57. framcore/timeindexes/OneYearProfileTimeIndex.py +10 -2
  58. framcore/timeindexes/ProfileTimeIndex.py +14 -3
  59. framcore/timeindexes/SinglePeriodTimeIndex.py +1 -1
  60. framcore/timeindexes/TimeIndex.py +16 -3
  61. framcore/timeindexes/WeeklyIndex.py +14 -2
  62. framcore/{expressions → timeindexes}/_time_vector_operations.py +76 -2
  63. framcore/timevectors/ConstantTimeVector.py +12 -16
  64. framcore/timevectors/LinearTransformTimeVector.py +20 -3
  65. framcore/timevectors/ListTimeVector.py +18 -14
  66. framcore/timevectors/LoadedTimeVector.py +1 -8
  67. framcore/timevectors/ReferencePeriod.py +13 -3
  68. framcore/timevectors/TimeVector.py +26 -12
  69. framcore/utils/__init__.py +0 -1
  70. framcore/utils/get_regional_volumes.py +21 -3
  71. framcore/utils/get_supported_components.py +1 -1
  72. framcore/utils/global_energy_equivalent.py +22 -5
  73. framcore/utils/isolate_subnodes.py +12 -3
  74. framcore/utils/loaders.py +7 -7
  75. framcore/utils/node_flow_utils.py +4 -4
  76. framcore/utils/storage_subsystems.py +3 -4
  77. fram_core-0.1.0a1.dist-info/RECORD +0 -100
  78. {fram_core-0.1.0a1.dist-info → fram_core-0.1.1.dist-info}/licenses/LICENSE.md +0 -0
@@ -1,18 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fram-core
3
- Version: 0.1.0a1
3
+ Version: 0.1.1
4
4
  Summary:
5
5
  License: LICENSE.md
6
6
  License-File: LICENSE.md
7
7
  Author: The Norwegian Water Resources and Energy Directorate
8
8
  Author-email: fram@nve.no
9
- Requires-Python: >=3.11
9
+ Requires-Python: >=3.11,<4
10
10
  Classifier: License :: Other/Proprietary License
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: juliacall (>=0.9.28,<0.10.0)
16
17
  Requires-Dist: numexpr (>=2.10.2)
17
18
  Requires-Dist: numpy (>=2.2.2)
18
19
  Requires-Dist: pandas (>=2.2.3)
@@ -23,11 +24,11 @@ Description-Content-Type: text/markdown
23
24
 
24
25
  ## About
25
26
 
26
- **fram-core** is the main package in **FRAM** modelling framework. The package contains essential features, interfaces and components for running energy market models in FRAM.
27
+ **fram-core** is the main package in the **FRAM** modelling framework. The package holds the functionality used to describe and manipulate the energy system, handle time series operations, and hold the definition of key interfaces in FRAM.
27
28
 
28
- For package documentation see [fram-core](https://nve.github.io/fram-core){:target="_blank"}.
29
+ For package documentation see [fram-core](https://nve.github.io/fram-core).
29
30
 
30
- For FRAM documentation see [FRAM mainpage](https://nve.github.io/fram){:target="_blank"}.
31
+ For FRAM documentation see [FRAM mainpage](https://nve.github.io/fram).
31
32
 
32
33
  ## Installation
33
34
 
@@ -0,0 +1,100 @@
1
+ framcore/Base.py,sha256=WAc8GBeuLxPpLFHbHwyYzd3HCv6RWo17iw0ItCRFXlE,6027
2
+ framcore/Model.py,sha256=S8Tdfl7Ynhhhk1KbOjCl0W4oZRNaroVGwjomwKZi9yU,3654
3
+ framcore/__init__.py,sha256=TvgesX44gH4OCopUSGY05BFK32oafI7ZDhctD6yQ57k,182
4
+ framcore/aggregators/Aggregator.py,sha256=sDlMZyCVwF-NhQPjMwcSm8xHH-SerBqlIP5ckbJ0C5A,8226
5
+ framcore/aggregators/HydroAggregator.py,sha256=XmaLPMe3xD1Vs2s7KsrSEy_zwLmDtCxMsslaCX5aJfg,47321
6
+ framcore/aggregators/NodeAggregator.py,sha256=QUPdXURkRIPymZAxHyavCzo1cGrnBJ3ahZNEmT9kwxE,23651
7
+ framcore/aggregators/WindSolarAggregator.py,sha256=5Fko0-FLRfUPMZQjpkWmpTwqzgW0WGm04zrUZ_-_Ypk,16465
8
+ framcore/aggregators/__init__.py,sha256=ZoBqilfv0XhFkYVROBPQhJHzSY1IcvhJmJ_FOo_Z5Pw,426
9
+ framcore/aggregators/_utils.py,sha256=EoRLeGmPC2ds-egPdvDewtaYbRozi6Psyf9DD1PRajk,8337
10
+ framcore/attributes/Arrow.py,sha256=bodWoKLG3g2bHt6FygwOJP1n3VKfAdrRfYCOveogrmk,11204
11
+ framcore/attributes/ElasticDemand.py,sha256=1sNXLAw1C-pGeQU2kgyoZF_-LTb3NCTOY5hOJwd7mrU,3073
12
+ framcore/attributes/ReservoirCurve.py,sha256=la4bgXNtR0DIdnQ1Iu37ydqr-sWRZN5yOVZ6qJ8rOZY,644
13
+ framcore/attributes/SoftBound.py,sha256=ooScf39EmqyWUIRafGiWEKGIvr6kMbV9JF4CIDARKBM,405
14
+ framcore/attributes/StartUpCost.py,sha256=tKT4hK23yQtjXWvrNz2EPC1oOxBO19SRfecxy77frU4,2107
15
+ framcore/attributes/Storage.py,sha256=c2p5rhaXpjB4PiPi6FCL7SB5X_DAu91bVSsRWE7-h1o,6284
16
+ framcore/attributes/TargetBound.py,sha256=5IcLwi1ciLm7SK3V9ZNzMDBeUKF62LfYr36ARQqJHQk,400
17
+ framcore/attributes/__init__.py,sha256=50viQ-upiArFHZpeUjEh9xqvuh-ZRGtIVwZNEtSGlI8,1530
18
+ framcore/attributes/hydro/HydroBypass.py,sha256=MdF51LNugwZhtpadBjiRI0zPA77Cnku5c8hTn42AJc8,1683
19
+ framcore/attributes/hydro/HydroGenerator.py,sha256=2uohj1QkZl52qNAR8nIywnLW-j_ujnxX-ITV2t9MRrU,4186
20
+ framcore/attributes/hydro/HydroPump.py,sha256=jKeJJk9kzz2dsxXfppqDcVf8_NHtwl6Yzlrv28k6z3M,7506
21
+ framcore/attributes/hydro/HydroReservoir.py,sha256=PEogXuSrdQ08uk5R_Re7G-Ue5N8cphPCEVvQhBIiy_U,879
22
+ framcore/attributes/hydro/__init__.py,sha256=isOSLP2TdHOaPcr16EXIkdtx80LVe8ztFPiuGJPC4ao,381
23
+ framcore/attributes/level_profile_attributes.py,sha256=feyIQ9lSnQGXdo45lU9kPCsKB1SNFrTfRaViFeNxlMI,33636
24
+ framcore/components/Component.py,sha256=z7ApRmaTPIZLwKJdbczrM8buLQnPJhBAgOixTLYG_zc,5581
25
+ framcore/components/Demand.py,sha256=c8N0R_f2GR58zt4pAXVmC-KcqLP5yeAuwQyEYQtHAtA,6241
26
+ framcore/components/Flow.py,sha256=slbVLlZsIxcEgilAJTe3eib4-xi2DTSAGHbpkh3KgFg,7391
27
+ framcore/components/HydroModule.py,sha256=5svph2fVlAYnMqKywdQv9hPsMLbHqSOdFIGwmkf8lMY,14302
28
+ framcore/components/Node.py,sha256=L_CDjjRHyxbiPCTjjj89dNQ9Z7VZjYp_dTHVodJN-DM,3661
29
+ framcore/components/Thermal.py,sha256=mOIIybvIDGhwsWN5ZCIch1Lkz8M1513S6uBI5bOil8s,7895
30
+ framcore/components/Transmission.py,sha256=u5opwBKWGylwDYYBELvGl8omHrBmMk4u4O5rE2hsdkA,7889
31
+ framcore/components/_PowerPlant.py,sha256=zDJ5LtYQN2GLa__BaqCLz54mZJil3dOYjICMPurh-U8,2787
32
+ framcore/components/__init__.py,sha256=0N4UMrYi7cYYaZHMIS6HSOli2jEbtct6LsZ0JcwGz5A,568
33
+ framcore/components/wind_solar.py,sha256=MSKr8MacjhDZLfKgWZf1NVyzurcMrGVjJhXfQzafzjQ,2311
34
+ framcore/curves/Curve.py,sha256=89CsAJpCa_GkQtsBGclUcK8D3ttCyYhs583-_6zE0is,934
35
+ framcore/curves/LoadedCurve.py,sha256=mW4OsG9WPEcm3rgPHZRMnMjqdf_PzYH2rYusN5PNGwQ,3639
36
+ framcore/curves/__init__.py,sha256=BnzdAZVjPwJcjduramRPPX6NJag-g6aKXPho2PwmGco,170
37
+ framcore/events/__init__.py,sha256=O3lOZukd_ixwbkoOmf2ei_--lKJLVXvyhSHjUDfaXEE,401
38
+ framcore/events/events.py,sha256=mNcHUjWraKyb-gwxeKr5ryOtcN8dCKptRWIYgcxM1p4,1683
39
+ framcore/expressions/Expr.py,sha256=qPbRGZxaWpn3y70dENth68DqkF3HP9dN-BVBp87yZWw,25554
40
+ framcore/expressions/__init__.py,sha256=Fn6a9aVCSU-R0QeGmZB7QuHkdLzP7Fvj_prnDLfwsrs,750
41
+ framcore/expressions/_get_constant_from_expr.py,sha256=3891ls8NQzVhTsvzvMsP7TG9Q5bE9jnOlBgA-N_Upjo,16429
42
+ framcore/expressions/_utils.py,sha256=mtruqnHcwpkIufbTFjJvT3iUfN98gN51SQ9e9hS9cUc,2123
43
+ framcore/expressions/queries.py,sha256=Hx3zOQPYR54cwttr1_NoS1i4HQRFpROfeBRqBLUWIF8,18077
44
+ framcore/expressions/units.py,sha256=RAcNTKbkuIs0svkIk6L7hPwA7-G8yHAaByB5BIku1kI,7715
45
+ framcore/fingerprints/__init__.py,sha256=YpIj-t6DRrSOmdidchC4qjLHSoZwsdkf9980nHUsnsw,256
46
+ framcore/fingerprints/fingerprint.py,sha256=GnvzaLquT15lXPGnWe103plCNMNgLzlY0ve1LUY8W5A,9179
47
+ framcore/juliamodels/JuliaModel.py,sha256=7USkw5Y4HKrSZP-Hmm-omoNXHqkow4NQqr0vuZOxIFU,7135
48
+ framcore/juliamodels/__init__.py,sha256=2ia9EGUQAZD0_f2o0NWdhD0d2z80K_bweohnGNfnpGA,124
49
+ framcore/loaders/__init__.py,sha256=iwlnauyXhH_BAuVIKVmX2vMvU94i_bXI57CE82eM7VA,209
50
+ framcore/loaders/loaders.py,sha256=-lMVdp1SDuUJ_kIGaEZTnl-_7keMCk0TOmSX0d-1efI,11118
51
+ framcore/metadata/Div.py,sha256=fIIB9W9fVJEUNzqIWXOJ9ZN3tWE8dB8ap2BdULT2fJw,2428
52
+ framcore/metadata/ExprMeta.py,sha256=nbpEBB2lkwRP1VqD9ohSKfGj99cVIX1g6hdVAocEXXQ,1679
53
+ framcore/metadata/LevelExprMeta.py,sha256=H_sqVFDP2NF6y4e1eq0ANH0Cq7ZdpSBuYaODtfWnO_4,1103
54
+ framcore/metadata/Member.py,sha256=w41UYW97BIXPr_yk514aaHmiYiYS8_Voh0bbE0qCleA,1715
55
+ framcore/metadata/Meta.py,sha256=P9ESFJlrxxhcY9PNSnnccrsLBulu1v3Kir6agzSrv0I,1173
56
+ framcore/metadata/__init__.py,sha256=8GfRSOVEb-051QiMCsgwYgqh-KA8UwSVdKpqtz1a3ZA,350
57
+ framcore/populators/Populator.py,sha256=SraN4QOTsA3yXEVG4z9ohWfPKokssl3uvDJPGh56lrU,4077
58
+ framcore/populators/__init__.py,sha256=jE6tHIvTaPTWcD-yVpBOjDBxI3xFqJh0QASfQexBLCE,119
59
+ framcore/querydbs/CacheDB.py,sha256=VZxNuZ9jX3a0ejDrUqAaZgBmFMTJG64v470-Imtl9i4,1738
60
+ framcore/querydbs/ModelDB.py,sha256=XD4jHlmRQHqF7L2euDIMrPZREhr3SXgv0UnqkADLzRw,1039
61
+ framcore/querydbs/QueryDB.py,sha256=IV96mtslktpJMrdxgrwQetVx4R1HAP1GxtKgx9WLPZA,1233
62
+ framcore/querydbs/__init__.py,sha256=oPX2sqAMoEMxWL9b_sRg4z4Q_b80iDm_jk-W_qmpEME,231
63
+ framcore/solvers/Solver.py,sha256=taiprQFC75pelr2koSmmxnJOeYtl7HsEi-0zFT6SR1k,2255
64
+ framcore/solvers/SolverConfig.py,sha256=QkowQHTfcB2f7v3_8U1zfkSewizzY7BHjT8LhSu8Bx0,10597
65
+ framcore/solvers/__init__.py,sha256=q9HLYJkRdvJeFgAM1NBGIaFI8APmsZCDTy1AkIKw_Zs,179
66
+ framcore/timeindexes/AverageYearRange.py,sha256=EDEkBxQNzTTQ2LtFubsTp9fWtBFWwlDyhPnaN91GJSY,1094
67
+ framcore/timeindexes/ConstantTimeIndex.py,sha256=eg8vsApymy1PkNEtHhx50L0eWFo9DB1xv4YXWbB35JU,787
68
+ framcore/timeindexes/DailyIndex.py,sha256=nORCghZjLvsuObRbC9SbR-NMBTng_DmjqULW2eJKh0M,1127
69
+ framcore/timeindexes/FixedFrequencyTimeIndex.py,sha256=BhIilAo6nG7-UEqPwJLS1T5ONA7g9RLrTIBIVapIk9E,33601
70
+ framcore/timeindexes/HourlyIndex.py,sha256=hdvoXmzzbATxjgO4uSyjo0v_GySkasmvs5_D6HzKrPc,1131
71
+ framcore/timeindexes/IsoCalendarDay.py,sha256=tm9XC5XEn6C_O5vrT94TNIm30XyMBpf7nCTVc0D9i-c,1144
72
+ framcore/timeindexes/ListTimeIndex.py,sha256=UjTTOiwLJ4D_BJPmuROQ6-mp01OV7aJ2sxnkn2iqcZI,11848
73
+ framcore/timeindexes/ModelYear.py,sha256=oq1Ef1gD-j6cteuQzx7eR8Ew9ZBL2QuDF0e1GVnu730,869
74
+ framcore/timeindexes/ModelYears.py,sha256=92UGocvqPtI6I01uJc6PKOInRVmYpZX3FcUnQtM2lxs,958
75
+ framcore/timeindexes/OneYearProfileTimeIndex.py,sha256=yKaan34cHSOypzzt_Q3nzn2ZZFy0R1bg7lChUEu--tQ,1154
76
+ framcore/timeindexes/ProfileTimeIndex.py,sha256=FDXXXEaXoImsiLSWUxYAFFOCzQpUb170sPk9hUuLkMs,1831
77
+ framcore/timeindexes/SinglePeriodTimeIndex.py,sha256=h9lkfIOlXuY-6OHULaVekTbtrSVCYgM5AjW0xTa1b1k,1452
78
+ framcore/timeindexes/TimeIndex.py,sha256=HPcyuJNoBrdvS7jAsQLLUIEYF9WvtT9cPL7dH2QX2a4,3515
79
+ framcore/timeindexes/WeeklyIndex.py,sha256=KiSQouztEW6dwphZwxYa8BsNpFQKs6FMWjrmneYnsz8,1160
80
+ framcore/timeindexes/__init__.py,sha256=2LmQFuMR1DffqHk5O2zd8o9g8QsazxSSykxOE7WGP4o,1351
81
+ framcore/timeindexes/_time_vector_operations.py,sha256=05HqseGkRqwztNk5rEI6p5VXFtXqmwArWuir88OFbnk,31301
82
+ framcore/timevectors/ConstantTimeVector.py,sha256=fp7Bu6nP8iIl0Pn9JLD0sMDMkldZ44d8bQNWWl2RvHQ,5651
83
+ framcore/timevectors/LinearTransformTimeVector.py,sha256=tmPICcT5mlGn60JDpQsQ5p3FXZyRbs5wehAGbH1sj2o,5435
84
+ framcore/timevectors/ListTimeVector.py,sha256=g8v4ym5NzjRJpHW9awbPKGQA4XidYPhBT_u8iSd5744,5429
85
+ framcore/timevectors/LoadedTimeVector.py,sha256=EmCSYwvvjzb7yQae2Y1C_QdY4sHsy4RARQfd06odGmU,4018
86
+ framcore/timevectors/ReferencePeriod.py,sha256=dnstzGIbK6Y2nxZqBEBueIGH44QmWva-HZ7-HTUctis,1712
87
+ framcore/timevectors/TimeVector.py,sha256=D8YOwdNo4TcXHVaTXCc1GiEJXrU85ve-v7QGcFMKeN8,3454
88
+ framcore/timevectors/__init__.py,sha256=Wd3gXGatwpbe2yMKSCq7O9My5wbveuK23iYO2ykkUIk,603
89
+ framcore/utils/__init__.py,sha256=qmmfcTWo6hrNyZn07m-A-6pcHwAUw5Y9It9ihapiiKY,1221
90
+ framcore/utils/get_regional_volumes.py,sha256=gTiXfKBPz4j7t-vyh49jBOQoszrBOMp8ZJViaOSZrA4,16041
91
+ framcore/utils/get_supported_components.py,sha256=yjLMmNwaHT8N9ZueQjw4v6z_rt5_aQVVtb7WAJ1-mzg,1885
92
+ framcore/utils/global_energy_equivalent.py,sha256=2k3PbORz6nwTdCriye7tJf4Ht5v6F1ZIetuSGmkfvnE,3445
93
+ framcore/utils/isolate_subnodes.py,sha256=Em1dc2shtWiF0IqxGj0CVjFF2A_6Ou64lsp88uAEcK8,6917
94
+ framcore/utils/loaders.py,sha256=pWOyRLSV6PikZHuUomKnZOL6c6VzXERysaMC_Q3mvfg,3201
95
+ framcore/utils/node_flow_utils.py,sha256=qnWelrqslc1FDBw-NdkVVQ1nk0l_EEvO8cDgObZnmJQ,7781
96
+ framcore/utils/storage_subsystems.py,sha256=LTKt6j3bCY7rvmOY3dUGcBP711CPXv5PS5XRb2DUmWM,4144
97
+ fram_core-0.1.1.dist-info/METADATA,sha256=7JuS8ef0hHwFatbcxvmbtSKPTmebjofuRj02SAxKnjs,1277
98
+ fram_core-0.1.1.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
99
+ fram_core-0.1.1.dist-info/licenses/LICENSE.md,sha256=fxh4ZxuR8dM2HDs-pIUitPrJdxQ4fEFh1GvEYZA2m1E,1075
100
+ fram_core-0.1.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.1
2
+ Generator: poetry-core 2.3.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
framcore/Base.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import contextlib
2
2
  import inspect
3
+ from collections.abc import Callable
3
4
  from typing import Any
4
5
 
5
6
  from framcore.events import (
@@ -18,9 +19,7 @@ class Base:
18
19
  """Core base class to share methods."""
19
20
 
20
21
  def _check_type(self, value, class_or_tuple) -> None: # noqa: ANN001
21
- if not isinstance(value, class_or_tuple):
22
- message = f"Expected {class_or_tuple} for {self}, got {type(value).__name__}"
23
- raise TypeError(message)
22
+ check_type(value, class_or_tuple, caller=self)
24
23
 
25
24
  def _ensure_float(self, value: object) -> float:
26
25
  with contextlib.suppress(Exception):
@@ -140,3 +139,23 @@ class Base:
140
139
  except Exception:
141
140
  pass
142
141
  return type(value).__name__
142
+
143
+
144
+ # could not place this in utils and use __init__ as modules in utils also import queries, if queries then import via utils __init__ we get circular imports.
145
+ def check_type(value: object, expected: type | tuple[type], caller: Callable | None = None) -> None:
146
+ """
147
+ Check a value matches expected type(s).
148
+
149
+ Args:
150
+ value (object): value being checked.
151
+ expected (type | tuple[type]): Expected types.
152
+ caller (Callable): The origin of the check.
153
+
154
+ Raises:
155
+ TypeError: When value does not match expected types.
156
+
157
+ """
158
+ if not isinstance(value, expected):
159
+ message = f"{expected}, got {type(value).__name__}"
160
+ message = "Expected " + message if caller is None else f"{caller} expected " + message
161
+ raise TypeError(message)
framcore/Model.py CHANGED
@@ -10,9 +10,12 @@ from framcore.timevectors import TimeVector
10
10
  if TYPE_CHECKING:
11
11
  from framcore.aggregators import Aggregator
12
12
 
13
+
13
14
  class ModelDict(dict):
14
15
  """Dict storing only values of type Component | Expr | TimeVector | Curve."""
15
- def __setitem__(self, key, value):
16
+
17
+ def __setitem__(self, key: str, value: Component | Expr | TimeVector | Curve) -> None:
18
+ """Set item with type checking."""
16
19
  if not isinstance(key, str):
17
20
  message = f"Expected str for key {key}, got {type(key).__name__}"
18
21
  raise TypeError(message)
@@ -21,24 +24,39 @@ class ModelDict(dict):
21
24
  raise TypeError(message)
22
25
  return super().__setitem__(key, value)
23
26
 
27
+
24
28
  class Model(Base):
25
- """Definition of the Model class."""
29
+ """
30
+ Model stores the representation of the energy system with Components, TimeVectors, Expression, and the Aggregators applied to the Model.
31
+
32
+ - Components describe the main elements in the energy system. Can have additional Attributes.
33
+ - TimeVector and Curve hold the time series data.
34
+ - Expressions for data manipulation of TimeVectors and Curves. Can be queried.
35
+ - Aggregators handle aggregation and disaggregation of Components. Aggregators are added to Model when used (Aggregator.aggregate(model)),
36
+ and can be undone in LIFO order with disaggregate().
37
+
38
+ Methods:
39
+ get_data(): Get dict of Components, Expressions, TimeVectors and Curves stored in the Model. Can be modified.
40
+ disaggregate(): Undo all aggregations applied to Model in LIFO order.
41
+ get_content_counts(): Return number of objects stored in model organized into concepts and types.
42
+
43
+ """
26
44
 
27
45
  def __init__(self) -> None:
28
- """Create a new model instance."""
46
+ """Create a new model instance with empty data and no aggregators."""
29
47
  self._data = ModelDict()
30
48
  self._aggregators: list[Aggregator] = []
31
49
 
50
+ def get_data(self) -> ModelDict:
51
+ """Get dict of Components, Expressions, TimeVectors and Curves stored in the Model. Can be modified."""
52
+ return self._data
53
+
32
54
  def disaggregate(self) -> None:
33
- """Undo all aggregations in LIFO order."""
55
+ """Undo all aggregations applied to Model in LIFO order."""
34
56
  while self._aggregators:
35
57
  aggregator = self._aggregators.pop(-1) # last item
36
58
  aggregator.disaggregate(self)
37
59
 
38
- def get_data(self) -> ModelDict:
39
- """Get internal data. Modify this with care."""
40
- return self._data
41
-
42
60
  def get_content_counts(self) -> dict[str, Counter]:
43
61
  """Return number of objects stored in model organized into concepts and types."""
44
62
  data_values = self.get_data().values()
@@ -70,4 +88,3 @@ class Model(Base):
70
88
  counts["aggregators"][type(a).__name__] += 1
71
89
 
72
90
  return counts
73
-
framcore/__init__.py CHANGED
@@ -1,9 +1,10 @@
1
1
  # framcore/__init__.py
2
-
2
+ from framcore.Base import check_type
3
3
  from framcore.Base import Base
4
4
  from framcore.Model import Model
5
5
 
6
6
  __all__ = [
7
7
  "Base",
8
8
  "Model",
9
+ "check_type",
9
10
  ]
@@ -18,17 +18,36 @@ class Aggregator(Base, ABC):
18
18
  """
19
19
  Aggregator interface class.
20
20
 
21
- Public API is the aggregate and disaggregate methods.
21
+ Aggregators handles aggregation and disaggregation of Components.
22
+ - The general approach for aggregation is to group Components, aggregate Components in the same group to (a) new Component(s),
23
+ delete the detailed Components, and add the mapping to self._aggregation_map.
24
+ - The general approach for disaggregation is to restore the detailed Components, move results from aggregated
25
+ Components to detailed Components, and delete the aggregated Components.
26
+
27
+ Concrete Aggregators must implement the abstract methods _aggregate() and _disaggregate().
28
+
29
+ Some rules for using Aggregators:
30
+ 1. Disaggragate can only be called after aggregate has been called.
31
+ 2. Not allowed to call aggregate twice. Must call disaggregate before aggregate can be called again.
32
+ 3. Aggregators are stored in Model when aggregate is called. Disaggregate by calling Model.disaggregate(),
33
+ which will disaggregate all Aggregators in LIFO order.
34
+ 4. At the moment we allow changes to the aggregated Components, which is ignored during disaggregation. TODO: Handle this
35
+ 5. It is recommended to only use the same Aggregator type once on the same components of a Model.
36
+ If you want to go from one aggregation level to another, it is better to use Model.disaggregate first and then aggregate again.
37
+ This is to keep the logic simple and avoid complex expressions.
38
+
39
+ Some design notes:
40
+ - Levels and profiles are aggregated separately and then combined into attributes.
41
+ - We have chosen to eagerly evaluate weights for aggregation (weighted averages) and disaggregation of levels and profiles.
42
+ This approach supports any form of aggregation by varying the weights, and complex weights can be created by eagerly evaluating
43
+ expressions and using the result to compute those weights.
44
+ - This is a balance between eagerly evaluating everything and setting up complex expressions.
45
+ Eagerly evaluating everything would require setting up new TimeVectors after evaluation, which is not ideal.
46
+ While setting up complex expressions gives expressions that are harder to work with and slower to query from.
47
+ - This trade-off simplifies adding logic that recognises if result expressions come from aggregations or disaggregations.
48
+ When aggregating or disaggregating these, we can go back to the original results rather than setting up complex expressions
49
+ that for examples aggregates the disaggregated results.
22
50
 
23
- These methods come with the folloing calling rules:
24
- 1. Not allowed to call aggregate twice. Must call disaggregate before aggregate can be called again.
25
- 2. Disaggragate can only be called after aggregate has been called.
26
-
27
- Implementations should implement _aggregate and _disaggregate.
28
- - The general approach for aggregation is to group components, aggregated components in the same group, delete the detailed components,
29
- and add the mapping to self._aggregation_map.
30
- - The general approach for disaggregation is to restore the detailed components, move results from aggregated components to detailed components,
31
- and delete the aggregated components.
32
51
  """
33
52
 
34
53
  def __init__(self) -> None:
@@ -42,7 +61,7 @@ class Aggregator(Base, ABC):
42
61
  self._check_type(model, Model)
43
62
 
44
63
  if self._is_last_call_aggregate is True:
45
- message = f"Will overwrite existing aggregation."
64
+ message = "Will overwrite existing aggregation."
46
65
  self.send_warning_event(message)
47
66
 
48
67
  self._original_data = deepcopy(model.get_data())
@@ -27,9 +27,10 @@ if TYPE_CHECKING:
27
27
 
28
28
  class HydroAggregator(Aggregator):
29
29
  """
30
- Aggregate hydro modules into two equivalent modules based on the regulation factor, into one regulated and one unregulated module per area.
30
+ Aggregate HydroModules into two equivalent modules based on the regulation factor, into one regulated and one unregulated module per area.
31
31
 
32
32
  Aggregation steps (self._aggregate):
33
+
33
34
  1. Group modules based on their power nodes (self._group_modules_by_power_node)
34
35
  - Modules with generators are grouped based on their power nodes. You can choose to only group modules for certain power nodes by giving
35
36
  self._power_node_members alone or together with self._metakey_power_node. NB! Watershed that crosses power nodes should not be aggregated in two
@@ -54,30 +55,24 @@ class HydroAggregator(Aggregator):
54
55
  3a. Aggregate results if all modules in group have results.
55
56
  - Production is the sum of production levels with weighted profiles
56
57
  - Reservoir filling is the sum of energy reservoir filling levels (filling*energy_equivalent_downstream/agg_energy_equivalent) with weighted profiles
57
- - TODO: Spill, bypass and pumping results are currently ignored in the aggregation.
58
+ - TODO: Water values, spill, bypass and pumping results are currently ignored in the aggregation.
58
59
  - TODO: Add possibility to skip results aggregation.
59
60
  3b. Make new hydro module and delete original modules from model data.
60
61
  4. Add mapping from detailed to aggregated modules to self._aggregation_map.
61
62
 
63
+
62
64
  Disaggregation steps (self._disaggregate):
65
+
63
66
  1. Restore original modules from self._original_data. NB! Changes to aggregated modules are lost except for results (TODO)
64
67
  2. Move production and filling results from aggregated modules to detailed modules, weighted based on production capacity and reservoir capacity.
65
- - TODO: Spill and bypass results are currently ignored in the disaggregation.
68
+ - TODO: Water values, spill, bypass and pumping results are currently ignored in the disaggregation.
66
69
  3. Delete aggregated modules.
67
70
 
68
71
  NB! Watershed that crosses power nodes should not be aggregated in two different HydroAggregators as the aggregator will remove all connected modules
69
72
  from the model after the first aggregation. Reservoirs will also be assigned to the power node which has the highest cumulative energy equivalent, so
70
73
  this aggregator does not work well for reservoirs that are upstream of multiple power nodes.
71
74
 
72
- Other comments:
73
- - It is recommended to only use the same aggregator type once on the same components of a model. If you want to go from one aggregation level to
74
- another, it is better to use model.disaggregate first and then aggregate again. This is to keep the logic simple and avoid complex expressions.
75
- We have also logic that recognises if result expressions come from aggregations or disaggregations. When aggregating or disaggregating these,
76
- we can go back to the original results rather than setting up complex expressions that for examples aggregates the disaggregated results.
77
- - Levels and profiles are aggregated separately, and then combined into attributes.
78
- - We have chosen to eagerly evaluate weights for aggregation and disaggregation of levels and profiles. This is a balance between eagerly evaluating
79
- everything, and setting up complex expressions. Eagerly evaluating everything would require setting up new timevectors after eager evaluation, which
80
- is not ideal. While setting up complex expressions gives expressions that are harder to work with and slower to query from.
75
+ See Aggregator for general design notes and rules to follow when using Aggregators.
81
76
 
82
77
  Attributes:
83
78
  _metakey_energy_eq_downstream (str): Metadata key for energy equivalent downstream.
@@ -93,6 +88,7 @@ class HydroAggregator(Aggregator):
93
88
  _release_capacity_profile (TimeVector | None): If given, use this profile for all aggregated modules' release capacities.
94
89
 
95
90
  Parent Attributes (see framcore.aggregators.Aggregator):
91
+
96
92
  _is_last_call_aggregate (bool | None): Tracks whether the last operation was an aggregation.
97
93
  _original_data (dict[str, Component | TimeVector | Curve | Expr] | None): Original detailed data before aggregation.
98
94
  _aggregation_map (dict[str, set[str]] | None): Maps aggregated components to their detailed components. detailed to agg
@@ -128,13 +124,15 @@ class HydroAggregator(Aggregator):
128
124
  super().__init__()
129
125
  self._check_type(metakey_energy_eq_downstream, str)
130
126
  self._check_type(ror_threshold, float)
131
- assert ror_threshold >= 0, ValueError(f"ror_threshold must be non-negative, got {ror_threshold}.")
132
127
  self._check_type(data_dim, SinglePeriodTimeIndex)
133
128
  self._check_type(scen_dim, FixedFrequencyTimeIndex)
134
129
  self._check_type(metakey_power_node, (str, type(None)))
135
130
  self._check_type(power_node_members, (list, type(None)))
136
- if metakey_power_node is not None:
137
- assert len(power_node_members) > 0, ValueError("If metakey_power_node is given, power_node_members must also be given.")
131
+ if ror_threshold < 0:
132
+ msg = f"ror_threshold must be non-negative, got {ror_threshold}."
133
+ raise ValueError(msg)
134
+ if metakey_power_node is not None and len(power_node_members) <= 0:
135
+ raise ValueError("If metakey_power_node is given, power_node_members must also be given.")
138
136
 
139
137
  self._metakey_energy_eq_downstream = metakey_energy_eq_downstream
140
138
  self._ror_threshold = ror_threshold
@@ -183,7 +181,7 @@ class HydroAggregator(Aggregator):
183
181
  for dd in d:
184
182
  if dd not in self._aggregation_map:
185
183
  self._aggregation_map[dd] = set([a])
186
- elif not data[dd].get_reservoir(): # if reservoir module already in map, skip as reservoir mapping is main mapping
184
+ elif not (data[dd].get_reservoir() and data[a].get_reservoir()): # reservoir modules can only be mapped to one aggregated reservoir module
187
185
  self._aggregation_map[dd].add(a)
188
186
  self.send_debug_event(f"add generator modules to _aggregation_map time: {round(time() - t, 3)} seconds")
189
187
 
@@ -450,7 +448,7 @@ class HydroAggregator(Aggregator):
450
448
  (
451
449
  mm,
452
450
  get_level_value(
453
- data[mm].get_generator().get_energy_eq().get_level() * data[mm].get_release_capacity().get_level(),
451
+ data[mm].get_generator().get_energy_equivalent().get_level() * data[mm].get_release_capacity().get_level(),
454
452
  model,
455
453
  "MW",
456
454
  self._data_dim,
@@ -466,7 +464,7 @@ class HydroAggregator(Aggregator):
466
464
 
467
465
  return ignore_production_capacity_modules
468
466
 
469
- def _aggregate_groups( # noqa: C901
467
+ def _aggregate_groups( # noqa: C901, PLR0915
470
468
  self,
471
469
  model: Model,
472
470
  upstream_topology: dict[str, list[str]],
@@ -487,10 +485,10 @@ class HydroAggregator(Aggregator):
487
485
 
488
486
  generator = HydroGenerator(
489
487
  power_node=data[generator_module_names[0]].get_generator().get_power_node(),
490
- energy_eq=Conversion(level=ConstantTimeVector(1.0, "kWh/m3", is_max_level=True)),
488
+ energy_equivalent=Conversion(level=ConstantTimeVector(1.0, "kWh/m3", is_max_level=True)),
491
489
  production=sum_production,
492
490
  )
493
- energy_eq = generator.get_energy_eq().get_level()
491
+ energy_eq = generator.get_energy_equivalent().get_level()
494
492
 
495
493
  # Release capacity
496
494
  release_capacities = [data[m].get_release_capacity() for m in generator_module_names if m not in ignore_capacity]
@@ -501,9 +499,16 @@ class HydroAggregator(Aggregator):
501
499
  release_capacities = deepcopy(release_capacities)
502
500
  for rc in release_capacities:
503
501
  rc.set_profile(self._release_capacity_profile)
504
- generator_energy_eqs = [data[m].get_generator().get_energy_eq() for m in generator_module_names if m not in ignore_capacity]
502
+ generator_energy_eqs = [data[m].get_generator().get_energy_equivalent() for m in generator_module_names if m not in ignore_capacity]
505
503
  release_capacity_levels = [rc.get_level() * ee.get_level() for rc, ee in zip(release_capacities, generator_energy_eqs, strict=True)]
506
- release_capacity = MaxFlowVolume(level=sum(release_capacity_levels) / energy_eq, profile=self._release_capacity_profile)
504
+
505
+ release_capacity_profile = None
506
+ if any(rc.get_profile() for rc in release_capacities):
507
+ one_profile_max = Expr(src=ConstantTimeVector(1.0, is_zero_one_profile=False), is_profile=True)
508
+ weights = [get_level_value(rcl, model, "MW", self._data_dim, self._scen_dim, is_max=True) for rcl in release_capacity_levels]
509
+ profiles = [rc.get_profile() if rc.get_profile() else one_profile_max for rc in release_capacities]
510
+ release_capacity_profile = _aggregate_weighted_expressions(profiles, weights)
511
+ release_capacity = MaxFlowVolume(level=sum(release_capacity_levels) / energy_eq, profile=release_capacity_profile)
507
512
 
508
513
  # Inflow level
509
514
  upstream_inflow_levels = defaultdict(list)
@@ -513,7 +518,7 @@ class HydroAggregator(Aggregator):
513
518
  if inflow:
514
519
  upstream_inflow_levels[m].append(inflow.get_level())
515
520
  inflow_level_energy = sum(
516
- sum(upstream_inflow_levels[m]) * data[m].get_generator().get_energy_eq().get_level()
521
+ sum(upstream_inflow_levels[m]) * data[m].get_generator().get_energy_equivalent().get_level()
517
522
  for m in generator_module_names
518
523
  if len(upstream_inflow_levels[m]) > 0
519
524
  )
@@ -524,7 +529,7 @@ class HydroAggregator(Aggregator):
524
529
  inflow_profile_to_energyinflow = defaultdict(list)
525
530
  inflow_level_to_value = dict()
526
531
  for m in generator_module_names:
527
- m_energy_eq = data[m].get_generator().get_energy_eq().get_level()
532
+ m_energy_eq = data[m].get_generator().get_energy_equivalent().get_level()
528
533
  m_energy_eq_value = get_level_value(
529
534
  m_energy_eq,
530
535
  db=model,
@@ -592,6 +597,13 @@ class HydroAggregator(Aggregator):
592
597
  """Aggregate reservoir fillings if all fillings are not None."""
593
598
  sum_filling = None
594
599
  if all(filling.get_level() for filling in fillings):
600
+ if any(not filling.get_profile() for filling in fillings):
601
+ missing = [member for member, filling in zip(members, fillings, strict=False) if not filling.get_profile()]
602
+ message = (
603
+ "Some reservoir fillings in grouped modules have no profile. Cannot aggregate profiles.",
604
+ f"Group: '{group_id}', missing profile for {missing}.",
605
+ )
606
+ raise ValueError(message)
595
607
  level, profiles, weights = self._get_level_profiles_weights_fillings(model, fillings, energy_eq_downstreams, energy_eq, weight_unit)
596
608
  profile = _aggregate_weighted_expressions(profiles, weights)
597
609
  sum_filling = StockVolume(level=level, profile=profile)
@@ -755,7 +767,7 @@ class HydroAggregator(Aggregator):
755
767
  for det in detailed_keys:
756
768
  det_module = data[det]
757
769
  release_capacity_level = det_module.get_release_capacity().get_level()
758
- generator_energy_eq = det_module.get_generator().get_energy_eq().get_level()
770
+ generator_energy_eq = det_module.get_generator().get_energy_equivalent().get_level()
759
771
  production_weight = get_level_value(
760
772
  release_capacity_level * generator_energy_eq,
761
773
  db=model,