linerate 2.1.3__tar.gz → 2.2.1__tar.gz

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 (103) hide show
  1. {linerate-2.1.3 → linerate-2.2.1}/.github/workflows/tests.yml +3 -3
  2. {linerate-2.1.3 → linerate-2.2.1}/.pre-commit-config.yaml +4 -0
  3. {linerate-2.1.3 → linerate-2.2.1}/PKG-INFO +1 -1
  4. {linerate-2.1.3 → linerate-2.2.1}/examples/plot_solar_heating_comparison.py +4 -1
  5. {linerate-2.1.3 → linerate-2.2.1}/examples/plot_solar_radiation.py +14 -11
  6. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/ieee738/convective_cooling.py +1 -1
  7. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/solar_angles.py +12 -6
  8. {linerate-2.1.3 → linerate-2.2.1}/linerate/models/cigre207.py +1 -1
  9. {linerate-2.1.3 → linerate-2.2.1}/linerate/models/cigre601.py +50 -35
  10. {linerate-2.1.3 → linerate-2.2.1}/linerate/models/ieee738.py +2 -4
  11. {linerate-2.1.3 → linerate-2.2.1}/linerate/models/thermal_model.py +7 -2
  12. {linerate-2.1.3 → linerate-2.2.1}/linerate/solver.py +18 -15
  13. {linerate-2.1.3 → linerate-2.2.1}/linerate/types.py +9 -12
  14. {linerate-2.1.3 → linerate-2.2.1}/linerate/units.py +1 -1
  15. {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/PKG-INFO +1 -1
  16. {linerate-2.1.3 → linerate-2.2.1}/pyproject.toml +6 -0
  17. {linerate-2.1.3 → linerate-2.2.1}/tests/conftest.py +13 -0
  18. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_solar_angles.py +3 -0
  19. {linerate-2.1.3 → linerate-2.2.1}/tests/integration_tests/test_vectorization.py +2 -0
  20. {linerate-2.1.3 → linerate-2.2.1}/tests/test_solver.py +31 -39
  21. {linerate-2.1.3 → linerate-2.2.1}/uv.lock +40 -1
  22. {linerate-2.1.3 → linerate-2.2.1}/.github/workflows/deploy.yml +0 -0
  23. {linerate-2.1.3 → linerate-2.2.1}/.gitignore +0 -0
  24. {linerate-2.1.3 → linerate-2.2.1}/LICENSE +0 -0
  25. {linerate-2.1.3 → linerate-2.2.1}/README.md +0 -0
  26. {linerate-2.1.3 → linerate-2.2.1}/docs/Makefile +0 -0
  27. {linerate-2.1.3 → linerate-2.2.1}/docs/_static/css/custom.css +0 -0
  28. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/cigre207/convective_cooling.rst +0 -0
  29. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/cigre207/solar_heating.rst +0 -0
  30. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/cigre601/convective_cooling.rst +0 -0
  31. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/cigre601/solar_heating.rst +0 -0
  32. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/convective_cooling.rst +0 -0
  33. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/dimensionless.rst +0 -0
  34. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/ieee738/convective_cooling.rst +0 -0
  35. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/ieee738/solar_heating.rst +0 -0
  36. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/index.rst +0 -0
  37. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/joule_heating.rst +0 -0
  38. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/math.rst +0 -0
  39. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/radiative_cooling.rst +0 -0
  40. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/solar_angles.rst +0 -0
  41. {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/solar_heating.rst +0 -0
  42. {linerate-2.1.3 → linerate-2.2.1}/docs/api/model.rst +0 -0
  43. {linerate-2.1.3 → linerate-2.2.1}/docs/api/solver.rst +0 -0
  44. {linerate-2.1.3 → linerate-2.2.1}/docs/api/types.rst +0 -0
  45. {linerate-2.1.3 → linerate-2.2.1}/docs/api.rst +0 -0
  46. {linerate-2.1.3 → linerate-2.2.1}/docs/bibliography.rst +0 -0
  47. {linerate-2.1.3 → linerate-2.2.1}/docs/conf.py +0 -0
  48. {linerate-2.1.3 → linerate-2.2.1}/docs/figs/frontpage.png +0 -0
  49. {linerate-2.1.3 → linerate-2.2.1}/docs/index.rst +0 -0
  50. {linerate-2.1.3 → linerate-2.2.1}/docs/make.bat +0 -0
  51. {linerate-2.1.3 → linerate-2.2.1}/docs/refs.bib +0 -0
  52. {linerate-2.1.3 → linerate-2.2.1}/examples/README.rst +0 -0
  53. {linerate-2.1.3 → linerate-2.2.1}/examples/__init__.py +0 -0
  54. {linerate-2.1.3 → linerate-2.2.1}/examples/plot_cigre.py +0 -0
  55. {linerate-2.1.3 → linerate-2.2.1}/linerate/__init__.py +0 -0
  56. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/__init__.py +0 -0
  57. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre207/__init__.py +0 -0
  58. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre207/ac_resistance.py +0 -0
  59. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre207/convective_cooling.py +0 -0
  60. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre207/solar_heating.py +0 -0
  61. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre601/__init__.py +0 -0
  62. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre601/convective_cooling.py +0 -0
  63. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre601/py.typed +0 -0
  64. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre601/solar_heating.py +0 -0
  65. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/convective_cooling.py +0 -0
  66. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/dimensionless.py +0 -0
  67. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/ieee738/__init__.py +0 -0
  68. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/ieee738/py.typed +0 -0
  69. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/ieee738/solar_heating.py +0 -0
  70. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/joule_heating.py +0 -0
  71. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/math.py +0 -0
  72. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/py.typed +0 -0
  73. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/radiative_cooling.py +0 -0
  74. {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/solar_heating.py +0 -0
  75. {linerate-2.1.3 → linerate-2.2.1}/linerate/model.py +0 -0
  76. {linerate-2.1.3 → linerate-2.2.1}/linerate/models/Cigre207.md +0 -0
  77. {linerate-2.1.3 → linerate-2.2.1}/linerate/py.typed +0 -0
  78. {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/SOURCES.txt +0 -0
  79. {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/dependency_links.txt +0 -0
  80. {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/requires.txt +0 -0
  81. {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/top_level.txt +0 -0
  82. {linerate-2.1.3 → linerate-2.2.1}/renovate.json +0 -0
  83. {linerate-2.1.3 → linerate-2.2.1}/setup.cfg +0 -0
  84. {linerate-2.1.3 → linerate-2.2.1}/tests/__init__.py +0 -0
  85. {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/__init__.py +0 -0
  86. {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/test_cigre_ampacity_cases.py +0 -0
  87. {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/test_ieee_ampacity_cases.py +0 -0
  88. {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/test_ratekit_ampacity_cases.py +0 -0
  89. {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/test_thermal_model.py +0 -0
  90. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/__init__.py +0 -0
  91. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/cigre207/test_convective_cooling.py +0 -0
  92. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/cigre601/__init__.py +0 -0
  93. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/cigre601/test_convective_cooling.py +0 -0
  94. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/cigre601/test_solar_heating.py +0 -0
  95. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/ieee738/test_ieee_convective_cooling.py +0 -0
  96. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/ieee738/test_solar_heating.py +0 -0
  97. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_convective_cooling.py +0 -0
  98. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_dimensionless.py +0 -0
  99. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_joule_heating.py +0 -0
  100. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_math.py +0 -0
  101. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_radiative_cooling.py +0 -0
  102. {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_solar_heating.py +0 -0
  103. {linerate-2.1.3 → linerate-2.2.1}/tests/models/test_thermal_model.py +0 -0
@@ -1,10 +1,10 @@
1
1
  ---
2
- name: Lint and Test
2
+ name: Lint, type check and test
3
3
 
4
4
  on: [push, pull_request]
5
5
 
6
6
  jobs:
7
- lint:
7
+ lint-and-type-check:
8
8
  runs-on: ubuntu-latest
9
9
  strategy:
10
10
  matrix:
@@ -17,7 +17,7 @@
17
17
  uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6
18
18
  with:
19
19
  python-version: ${{ matrix.python-version }}
20
- - name: Run linting
20
+ - name: Run linting and type checking
21
21
  run: |
22
22
  uv run --frozen pre-commit run --all-files
23
23
 
@@ -21,3 +21,7 @@ repos:
21
21
  - id: ruff
22
22
  args: [--fix, --exit-non-zero-on-fix]
23
23
  - id: ruff-format
24
+ - repo: https://github.com/RobertCraigie/pyright-python
25
+ rev: v1.1.408
26
+ hooks:
27
+ - id: pyright
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linerate
3
- Version: 2.1.3
3
+ Version: 2.2.1
4
4
  Summary: Library for computing line ampacity ratings for overhead lines
5
5
  Author-email: Statnett Datascience <Datascience.Drift@Statnett.no>, Yngve Mardal Moe <yngve.m.moe@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -73,7 +73,10 @@ for k, v in vals_with_range.items():
73
73
  # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
74
74
 
75
75
  P_s_cigre = solar_heating.compute_solar_heating(alpha_s, I_T, D)
76
+ assert isinstance(P_s_cigre, np.ndarray)
77
+
76
78
  P_s_ieee = ieee738.solar_heating.compute_solar_heating(alpha_s, Q_se, cos_theta, D)
79
+ assert isinstance(P_s_ieee, np.ndarray)
77
80
 
78
81
  ###############################################################################
79
82
  # Create visualisation
@@ -92,7 +95,7 @@ for k, v in vals_with_range.items():
92
95
  )
93
96
  elif k == "sin_H_s":
94
97
  plt.xlabel("Solar altitude [$^\\circ$]")
95
- plt.xticks(ticks=[0, 1 / 3, 2 / 3, 1], labels=[0, 30, 60, 90])
98
+ plt.xticks(ticks=[0, 1 / 3, 2 / 3, 1], labels=["0", "30", "60", "90"])
96
99
  plt.title(
97
100
  r"Solar heating calculated using the CIGRE-601 and the IEEE-736 standards."
98
101
  "\n"
@@ -11,7 +11,6 @@ the difference between the solar azimuth and the span azimuth (or bearing),
11
11
  # Imports and utilities
12
12
  # ^^^^^^^^^^^^^^^^^^^^^
13
13
 
14
- import matplotlib.cm as cm
15
14
  import matplotlib.pyplot as plt
16
15
  import numpy as np
17
16
 
@@ -45,9 +44,12 @@ I_d = cigre601.solar_heating.compute_diffuse_sky_radiation(I_B, sin_H_s)
45
44
  I_T_F0 = cigre601.solar_heating.compute_global_radiation_intensity(
46
45
  I_B, I_d, albedo=0.0, sin_angle_of_sun_on_line=sin_eta, sin_solar_altitude=sin_H_s
47
46
  )
47
+ assert isinstance(I_T_F0, np.ndarray)
48
+
48
49
  I_T_F = cigre601.solar_heating.compute_global_radiation_intensity(
49
50
  I_B, I_d, albedo=albedo, sin_angle_of_sun_on_line=sin_eta, sin_solar_altitude=sin_H_s
50
51
  )
52
+ assert isinstance(I_T_F, np.ndarray)
51
53
 
52
54
  ###############################################################################
53
55
  # Create visualisation
@@ -57,19 +59,20 @@ I_T_F = cigre601.solar_heating.compute_global_radiation_intensity(
57
59
 
58
60
  # Setup figure and axes
59
61
  fig = plt.figure(figsize=(11, 1.9))
60
- axes = [fig.add_axes([0.07, 0.27, 0.19, 0.7])]
62
+ axes = [fig.add_axes((0.07, 0.27, 0.19, 0.7))]
61
63
  axes += [
62
- fig.add_axes([0.30, 0.27, 0.18, 0.7], sharey=axes[0]),
63
- fig.add_axes([0.53, 0.27, 0.18, 0.7], sharey=axes[0]),
64
- fig.add_axes([0.76, 0.27, 0.18, 0.7], sharey=axes[0]),
64
+ fig.add_axes((0.30, 0.27, 0.18, 0.7), sharey=axes[0]),
65
+ fig.add_axes((0.53, 0.27, 0.18, 0.7), sharey=axes[0]),
66
+ fig.add_axes((0.76, 0.27, 0.18, 0.7), sharey=axes[0]),
65
67
  ]
66
- cbar_ax = fig.add_axes([0.955, 0.27, 0.015, 0.7])
68
+ cbar_ax = fig.add_axes((0.955, 0.27, 0.015, 0.7))
67
69
 
68
70
  # Add plots
69
71
  axes[0].plot(np.degrees(solar_altitude), I_B, color="k")
70
72
  axes[1].plot(np.degrees(solar_altitude), I_d, color="k")
73
+ cmap = plt.get_cmap("cividis")
71
74
  for i, d_gamma in enumerate(azimuth_difference.ravel()):
72
- color = cm.cividis(d_gamma / azimuth_difference.max())
75
+ color = cmap(d_gamma / azimuth_difference.max())
73
76
  d_gamma = np.degrees(d_gamma)
74
77
  axes[2].plot(np.degrees(solar_altitude), I_T_F0[:, i], color=color)
75
78
  axes[3].plot(np.degrees(solar_altitude), I_T_F[:, i], color=color)
@@ -84,7 +87,7 @@ for ax in axes:
84
87
  # Setup y-axes to be shared
85
88
  ax.set_ylim(0, I_T_F.max() * 1.05)
86
89
  ax.set_yticks([0, 500, 1000, 1360]) # Include tick for solar constant
87
- ax.set_yticklabels([0, 500, 1000, "$G_{SC}$"])
90
+ ax.set_yticklabels(["0", "500", "1000", "$G_{SC}$"])
88
91
  ax.axhline(1360, color="k", linestyle="--") # Add dashed line for solar constant
89
92
 
90
93
 
@@ -96,8 +99,8 @@ axes[3].tick_params(labelleft=False)
96
99
  # Setup labels
97
100
  axes[0].set_ylabel(r"$I_B~[\mathrm{W}~\mathrm{m}^{-1}]$", labelpad=-1)
98
101
  axes[1].set_ylabel(r"$I_d~[\mathrm{W}~\mathrm{m}^{-1}]$")
99
- axes[2].set_ylabel("$F=0$\n$I_T~[\mathrm{W}~\mathrm{m}^{-1}]$") # noqa
100
- axes[3].set_ylabel(f"$F={albedo}$\n$I_T~[\mathrm{{W}}~\mathrm{{m}}^{{-1}}]$") # noqa
102
+ axes[2].set_ylabel(r"$F=0$\n$I_T~[\mathrm{W}~\mathrm{m}^{-1}]$") # noqa
103
+ axes[3].set_ylabel(rf"$F={albedo}$\n$I_T~[\mathrm{{W}}~\mathrm{{m}}^{{-1}}]$") # noqa
101
104
 
102
105
  # Colorbar
103
106
  cbar_ax.imshow(azimuth_difference.T, aspect="auto", cmap="cividis")
@@ -105,7 +108,7 @@ cbar_ax.yaxis.set_label_position("right")
105
108
  cbar_ax.set_ylabel(r"$\left|\gamma_c - \gamma_s\right|~[^\circ]$", labelpad=-10)
106
109
  cbar_ax.yaxis.tick_right()
107
110
  cbar_ax.set_yticks(cbar_ax.get_ylim())
108
- cbar_ax.set_yticklabels([0, 90])
111
+ cbar_ax.set_yticklabels(["0", "90"])
109
112
  cbar_ax.set_xticks([])
110
113
 
111
114
  plt.show()
@@ -190,7 +190,7 @@ def compute_forced_convection( # q_c1 or q_c2
190
190
  q_c1 = K_angle * (1.01 + 1.35 * N_Re**0.52) * k_f * (T_s - T_a)
191
191
  q_c2 = K_angle * 0.754 * N_Re**0.6 * k_f * (T_s - T_a)
192
192
 
193
- if hasattr(q_c1, "__len__"):
193
+ if isinstance(q_c1, np.ndarray) and isinstance(q_c2, np.ndarray):
194
194
  q_cf = []
195
195
  for i in range(len(q_c1)):
196
196
  if q_c1[i] > q_c2[i]:
@@ -1,16 +1,17 @@
1
1
  import numpy as np
2
2
  from numba import vectorize
3
+
3
4
  from linerate.equations import math
4
5
 
5
- from ..units import Date, Degrees, Radian, Unitless
6
6
  from ..types import Span
7
+ from ..units import Date, Degrees, Radian, Unitless
7
8
 
8
9
 
9
10
  def _get_day_of_year(when: Date) -> Unitless:
10
11
  YearResolutionType = np.datetime64(1, "Y")
11
12
  DayResolutionType = np.datetime64(1, "D")
12
13
 
13
- return (when.astype(DayResolutionType) - when.astype(YearResolutionType)).astype(float) + 1
14
+ return (when.astype(DayResolutionType) - when.astype(YearResolutionType)).astype(float) + 1.0
14
15
 
15
16
 
16
17
  def _get_hour_of_day(when: Date) -> Unitless:
@@ -55,9 +56,11 @@ def compute_hour_angle_relative_to_noon(when: Date, longitude: Degrees) -> Radia
55
56
  utc_minute = _get_minute_of_hour(when)
56
57
  pi = np.pi
57
58
  # We add longitude/15 since 15 degrees of longitude increases solar hour by 1
58
- return np.mod((-12 + utc_hour + utc_minute / 60 + longitude / 15), 24) * (
59
+ hour_angle = np.mod((-12 + utc_hour + utc_minute / 60 + longitude / 15), 24) * (
59
60
  pi / 12
60
61
  ) # pi/12 is 15 degrees
62
+ # Shift to [-pi, pi] range to ensure negative values before noon
63
+ return np.where(hour_angle >= pi, hour_angle - 2 * pi, hour_angle)
61
64
 
62
65
 
63
66
  def compute_solar_declination(
@@ -123,13 +126,16 @@ def _compute_solar_azimuth_constant(
123
126
  if -pi <= omega < 0:
124
127
  if chi >= 0:
125
128
  C = 0
126
- elif chi < 0:
129
+ else:
127
130
  C = pi
128
131
  elif 0 <= omega < pi:
129
132
  if chi >= 0:
130
133
  C = pi
131
- elif chi < 0:
134
+ else:
132
135
  C = 2 * pi
136
+ else:
137
+ raise ValueError(f"Hour angle {omega} out of range [-π, π)")
138
+
133
139
  return C
134
140
 
135
141
 
@@ -138,7 +144,7 @@ def compute_solar_azimuth_constant(
138
144
  ) -> Radian:
139
145
  r"""Compute the solar azimuth constant.
140
146
 
141
- Table 2 on page 18 of:cite:p:`ieee738`.
147
+ Table 2 on page 18 of :cite:p:`ieee738`.
142
148
 
143
149
  Parameters
144
150
  ----------
@@ -60,7 +60,7 @@ class Cigre207(ThermalModel):
60
60
  )
61
61
  if self.include_diffuse_radiation:
62
62
  I_d = cigre207.solar_heating.compute_diffuse_sky_radiation(I_B, sin_H_s)
63
- F = self.span.ground_albedo
63
+ F = self.weather.ground_albedo
64
64
  else:
65
65
  I_d = 0
66
66
  F = 0
@@ -1,5 +1,3 @@
1
- from numbers import Real
2
-
3
1
  import numpy as np
4
2
 
5
3
  from linerate.equations import (
@@ -11,24 +9,25 @@ from linerate.equations import (
11
9
  solar_heating,
12
10
  )
13
11
  from linerate.models.thermal_model import ThermalModel, _copy_method_docstring
14
- from linerate.types import Span, Weather, WeatherWithSolarRadiation
12
+ from linerate.types import BaseWeather, Span, Weather, WeatherWithSolarRadiation
15
13
  from linerate.units import (
16
14
  Ampere,
17
15
  Celsius,
18
16
  Date,
19
17
  JoulePerKilogramPerKelvin,
20
18
  OhmPerMeter,
19
+ Unitless,
21
20
  WattPerMeter,
22
21
  )
23
22
 
24
23
 
25
- class Cigre601(ThermalModel):
24
+ class BaseCigre601(ThermalModel):
26
25
  def __init__(
27
26
  self,
28
27
  span: Span,
29
- weather: Weather,
28
+ weather: BaseWeather,
30
29
  time: Date,
31
- max_reynolds_number: Real = 4000, # Max value of the angle correction in CIGRE601
30
+ max_reynolds_number: Unitless = 4000.0, # Max value of the angle correction in CIGRE601
32
31
  ):
33
32
  super().__init__(span, weather)
34
33
  self.time = time
@@ -48,33 +47,6 @@ class Cigre601(ThermalModel):
48
47
  conductor_temperature=conductor_temperature, current=current
49
48
  )
50
49
 
51
- @_copy_method_docstring(ThermalModel)
52
- def compute_solar_heating(
53
- self, conductor_temperature: Celsius, current: Ampere
54
- ) -> WattPerMeter:
55
- alpha_s = self.span.conductor.solar_absorptivity
56
- F = self.weather.ground_albedo
57
- y = self.span.conductor_altitude
58
- N_s = self.weather.clearness_ratio
59
- D = self.span.conductor.conductor_diameter
60
-
61
- sin_H_s = solar_angles.compute_sin_solar_altitude_for_span(self.span, self.time)
62
-
63
- sin_eta = solar_angles.compute_sin_solar_effective_incidence_angle_for_span(
64
- self.span, self.time, sin_H_s
65
- )
66
-
67
- I_B = cigre601.solar_heating.compute_direct_solar_radiation(sin_H_s, N_s, y)
68
- I_d = cigre601.solar_heating.compute_diffuse_sky_radiation(I_B, sin_H_s)
69
- I_T = cigre601.solar_heating.compute_global_radiation_intensity(
70
- I_B, I_d, F, sin_eta, sin_H_s
71
- )
72
- return solar_heating.compute_solar_heating(
73
- alpha_s,
74
- I_T,
75
- D,
76
- )
77
-
78
50
  @_copy_method_docstring(ThermalModel)
79
51
  def compute_convective_cooling(
80
52
  self, conductor_temperature: Celsius, current: Ampere
@@ -169,12 +141,55 @@ class Cigre601(ThermalModel):
169
141
  )
170
142
 
171
143
 
172
- class Cigre601WithSolarRadiation(Cigre601):
144
+ class Cigre601(BaseCigre601):
145
+ def __init__(
146
+ self,
147
+ span: Span,
148
+ weather: Weather,
149
+ time: Date,
150
+ max_reynolds_number: Unitless = 4000.0, # Max value of the angle correction in CIGRE601
151
+ ):
152
+ self.span = span
153
+ self.weather = weather
154
+ self.time = time
155
+ self.max_reynolds_number = max_reynolds_number
156
+
157
+ @_copy_method_docstring(ThermalModel)
158
+ def compute_solar_heating(
159
+ self, conductor_temperature: Celsius, current: Ampere
160
+ ) -> WattPerMeter:
161
+ alpha_s = self.span.conductor.solar_absorptivity
162
+ F = self.weather.ground_albedo
163
+ y = self.span.conductor_altitude
164
+ N_s = self.weather.clearness_ratio
165
+ D = self.span.conductor.conductor_diameter
166
+
167
+ sin_H_s = solar_angles.compute_sin_solar_altitude_for_span(self.span, self.time)
168
+
169
+ sin_eta = solar_angles.compute_sin_solar_effective_incidence_angle_for_span(
170
+ self.span, self.time, sin_H_s
171
+ )
172
+
173
+ I_B = cigre601.solar_heating.compute_direct_solar_radiation(sin_H_s, N_s, y)
174
+ I_d = cigre601.solar_heating.compute_diffuse_sky_radiation(I_B, sin_H_s)
175
+ I_T = cigre601.solar_heating.compute_global_radiation_intensity(
176
+ I_B, I_d, F, sin_eta, sin_H_s
177
+ )
178
+ return solar_heating.compute_solar_heating(
179
+ alpha_s,
180
+ I_T,
181
+ D,
182
+ )
183
+
184
+
185
+ class Cigre601WithSolarRadiation(BaseCigre601):
173
186
  """Extension of the Cigre601 model that accepts external solar radiation data for direct and diffuse solar
174
187
  radiation."""
175
188
 
176
189
  def __init__(self, span: Span, weather: WeatherWithSolarRadiation, time: Date):
177
- super().__init__(span, weather, time)
190
+ self.span = span
191
+ self.weather = weather
192
+ self.time = time
178
193
  self.weather = weather
179
194
 
180
195
  def compute_solar_heating(
@@ -1,11 +1,9 @@
1
- from numbers import Real
2
-
3
1
  import numpy as np
4
2
 
5
3
  from linerate.equations import dimensionless, ieee738, math, solar_angles
6
4
  from linerate.models.thermal_model import ThermalModel, _copy_method_docstring
7
5
  from linerate.types import Span, Weather
8
- from linerate.units import Ampere, Celsius, Date, OhmPerMeter, WattPerMeter
6
+ from linerate.units import Ampere, Celsius, Date, OhmPerMeter, Unitless, WattPerMeter
9
7
 
10
8
 
11
9
  class IEEE738(ThermalModel):
@@ -14,7 +12,7 @@ class IEEE738(ThermalModel):
14
12
  span: Span,
15
13
  weather: Weather,
16
14
  time: Date,
17
- max_reynolds_number: Real = 50_000, # Max Reynolds number for forced convection
15
+ max_reynolds_number: Unitless = 50_000.0, # Max Reynolds number for forced convection
18
16
  ):
19
17
  super().__init__(span, weather)
20
18
  self.time = time
@@ -3,7 +3,7 @@ from typing import Dict
3
3
 
4
4
  from linerate import solver
5
5
  from linerate.equations import joule_heating, radiative_cooling
6
- from linerate.types import Span, Weather
6
+ from linerate.types import BaseWeather, Span
7
7
  from linerate.units import Ampere, Celsius, OhmPerMeter, WattPerMeter
8
8
 
9
9
 
@@ -19,7 +19,7 @@ class ThermalModel(ABC):
19
19
  """Abstract class for a minimal conductor thermal model."""
20
20
 
21
21
  @abstractmethod
22
- def __init__(self, span: Span, weather: Weather):
22
+ def __init__(self, span: Span, weather: BaseWeather):
23
23
  self.span = span
24
24
  self.weather = weather
25
25
 
@@ -198,6 +198,7 @@ class ThermalModel(ABC):
198
198
  min_ampacity: Ampere = 0,
199
199
  max_ampacity: Ampere = 5000,
200
200
  tolerance: float = 1.0,
201
+ accept_invalid_values: bool = False,
201
202
  ) -> Ampere:
202
203
  r"""Use the bisection method to compute the steady-state thermal rating (ampacity).
203
204
 
@@ -216,6 +217,9 @@ class ThermalModel(ABC):
216
217
  bisection iterations will stop once the numerical ampacity uncertainty is below
217
218
  :math:`\Delta I`. The bisection method will run for
218
219
  :math:`\left\lceil\frac{I_\text{min} - I_\text{min}}{\Delta I}\right\rceil` iterations.
220
+ accept_invalid_values:
221
+ If True, np.nan is returned whenever the current cannot be found within the provided
222
+ search interval. If False, a ValueError will be raised instead.
219
223
 
220
224
  Returns
221
225
  -------
@@ -228,6 +232,7 @@ class ThermalModel(ABC):
228
232
  min_ampacity=min_ampacity,
229
233
  max_ampacity=max_ampacity,
230
234
  tolerance=tolerance,
235
+ accept_invalid_values=accept_invalid_values,
231
236
  )
232
237
  n = self.span.num_conductors
233
238
  return I * n
@@ -1,5 +1,5 @@
1
1
  from functools import partial
2
- from typing import Callable, Optional
2
+ from typing import Callable
3
3
 
4
4
  import numpy as np
5
5
 
@@ -13,7 +13,7 @@ def bisect(
13
13
  xmin: FloatOrFloatArray,
14
14
  xmax: FloatOrFloatArray,
15
15
  tolerance: float,
16
- invalid_value: Optional[float] = None,
16
+ accept_invalid_values: bool = False,
17
17
  ) -> FloatOrFloatArray:
18
18
  r"""Compute the roots of a function using a vectorized bisection method.
19
19
 
@@ -32,10 +32,10 @@ def bisect(
32
32
  bounded within an interval of size :math:`\Delta x` or less. The bisection method will
33
33
  run for :math:`\left\lceil\frac{x_\max - x_\min}{\Delta x}\right\rceil`
34
34
  iterations.
35
- invalid_value:
36
- This value is used whenever
37
- :math:`\text{sign}(f(\mathbf{x}_\min)) = \text{sign}(f(\mathbf{x}_\max))`. If not provided
38
- np.nan is used.
35
+ accept_invalid_values:
36
+ If True, np.nan is returned whenever
37
+ :math:`\text{sign}(f(\mathbf{x}_\min)) = \text{sign}(f(\mathbf{x}_\max))`
38
+ If False, a ValueError will be raised.
39
39
 
40
40
  Returns
41
41
  -------
@@ -44,7 +44,7 @@ def bisect(
44
44
  there is a root :math:`x_i \in [\tilde{x}_i - 0.5 \Delta x, \tilde{x}_i + 0.5 \Delta x]`
45
45
  so :math:`f_i(x_i) = 0`.
46
46
  """
47
- _invalid_value = np.nan if invalid_value is None else invalid_value
47
+ _invalid_value = np.nan
48
48
 
49
49
  if not np.all(np.isfinite(xmin)) or not np.all(np.isfinite(xmax)):
50
50
  raise ValueError("xmin and xmax must be finite.")
@@ -54,7 +54,7 @@ def bisect(
54
54
  f_right = f(xmax)
55
55
 
56
56
  invalid_mask = np.sign(f_left) == np.sign(f_right)
57
- if np.any(invalid_mask) and invalid_value is None:
57
+ if np.any(invalid_mask) and not accept_invalid_values:
58
58
  raise ValueError(
59
59
  "f(xmin) and f(xmax) have the same sign. Consider increasing the search interval."
60
60
  )
@@ -114,7 +114,9 @@ def compute_conductor_temperature(
114
114
  Union[float, float64, ndarray[Any, dtype[float64]]]
115
115
  :math:`I~\left[\text{A}\right]`. The thermal rating.
116
116
  """
117
- f = partial(heat_balance, current=current)
117
+
118
+ def f(conductor_temperature: Celsius) -> WattPerMeter:
119
+ return heat_balance(conductor_temperature, current)
118
120
 
119
121
  return bisect(f, min_temperature, max_temperature, tolerance)
120
122
 
@@ -125,7 +127,7 @@ def compute_conductor_ampacity(
125
127
  min_ampacity: Ampere = 0,
126
128
  max_ampacity: Ampere = 5_000,
127
129
  tolerance: float = 1, # Ampere
128
- invalid_value=None,
130
+ accept_invalid_values: bool = False,
129
131
  ) -> Ampere:
130
132
  r"""Use the bisection method to compute the steady-state thermal rating (ampacity).
131
133
 
@@ -148,10 +150,9 @@ def compute_conductor_ampacity(
148
150
  bisection iterations will stop once the numerical ampacity uncertainty is below
149
151
  :math:`\Delta I`. The bisection method will run for
150
152
  :math:`\left\lceil\frac{I_\text{max} - I_\text{min}}{\Delta I}\right\rceil` iterations.
151
- invalid_value:
152
- if the optimization problem is invalid, this value is returned instead of an error.
153
- Suggested value: 0 for 0-ampacity when max_conductor_temperature is exceeded for all
154
- ampacities.
153
+ accept_invalid_values:
154
+ If True, np.nan is returned whenever the current cannot be found within the provided
155
+ search interval. If False, a ValueError will be raised instead.
155
156
 
156
157
  Returns
157
158
  -------
@@ -160,4 +161,6 @@ def compute_conductor_ampacity(
160
161
  """
161
162
  f = partial(heat_balance, max_conductor_temperature)
162
163
 
163
- return bisect(f, min_ampacity, max_ampacity, tolerance, invalid_value=invalid_value)
164
+ return bisect(
165
+ f, min_ampacity, max_ampacity, tolerance, accept_invalid_values=accept_invalid_values
166
+ )
@@ -163,8 +163,8 @@ class Span:
163
163
  return 0.5 * (self.start_tower.altitude + self.end_tower.altitude)
164
164
 
165
165
 
166
- @dataclass()
167
- class Weather:
166
+ @dataclass
167
+ class BaseWeather:
168
168
  #: :math:`T_a~\left[^\circ C\right]`. The ambient air temperature.
169
169
  air_temperature: Celsius
170
170
  #: :math:`\delta~\left[\text{radian}\right]`. Wind direction east of north.
@@ -173,24 +173,21 @@ class Weather:
173
173
  wind_speed: MeterPerSecond
174
174
  #: :math:`F`. The ground albedo.
175
175
  ground_albedo: Unitless
176
+
177
+
178
+ @dataclass
179
+ class Weather(BaseWeather):
176
180
  #: :math:`N_s`. The clearness ratio (or clearness number in
177
181
  #: :cite:p:`sharma1965interrelationships,cigre207`).
178
182
  clearness_ratio: Unitless = 1
179
183
 
180
184
 
181
185
  @dataclass
182
- class WeatherWithSolarRadiation(Weather):
186
+ class WeatherWithSolarRadiation(BaseWeather):
183
187
  """Extension of the Weather class to accept solar radiation timeseries."""
184
188
 
185
189
  #: :math:`I_d~\left[\text{W}~\text{m}^{-2}\right]`. The diffuse radiation intensity.
186
- diffuse_radiation_intensity: WattPerSquareMeter = None
190
+ diffuse_radiation_intensity: WattPerSquareMeter
187
191
  #: :math:`I_B~\left[\text{W}~\text{m}^{-2}\right]`. The direct radiation intensity on a surface normal to the
188
192
  # sun's beam.
189
- direct_radiation_intensity: WattPerSquareMeter = None
190
-
191
- def __post_init__(self):
192
- if (self.diffuse_radiation_intensity is None) or (self.direct_radiation_intensity is None):
193
- raise ValueError(
194
- "Both 'diffuse_radiation_intensity' and 'direct_radiation_intensity' must be provided. For weather"
195
- " data without solar radiation, use the 'Weather' class instead.",
196
- )
193
+ direct_radiation_intensity: WattPerSquareMeter
@@ -8,7 +8,7 @@ except ImportError: # Python version <3.9
8
8
  import numpy as np
9
9
  import numpy.typing as npt
10
10
 
11
- FloatOrFloatArray = Union[float, np.float64, npt.NDArray[np.float64]]
11
+ FloatOrFloatArray = Union[float, np.floating, npt.NDArray[np.floating]]
12
12
  BoolOrBoolArray = Union[bool, np.bool_, npt.NDArray[np.bool_]]
13
13
 
14
14
  OhmPerMeter = Annotated[FloatOrFloatArray, "Ω/m"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linerate
3
- Version: 2.1.3
3
+ Version: 2.2.1
4
4
  Summary: Library for computing line ampacity ratings for overhead lines
5
5
  Author-email: Statnett Datascience <Datascience.Drift@Statnett.no>, Yngve Mardal Moe <yngve.m.moe@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -24,6 +24,8 @@ dev = [
24
24
  "pytest==8.4.2",
25
25
  "pytest-cov==7.0.0",
26
26
  "pytest-randomly==4.0.1",
27
+ "pyright[nodejs]",
28
+ "matplotlib>=3.9.4",
27
29
  ]
28
30
  docs = [
29
31
  "sphinx",
@@ -94,3 +96,7 @@ filterwarnings = [
94
96
  [build-system]
95
97
  requires = ["setuptools>=64", "setuptools-scm>=8"]
96
98
  build-backend = "setuptools.build_meta"
99
+
100
+ [tool.pyright]
101
+ venvPath = "."
102
+ venv = ".venv"
@@ -1,8 +1,11 @@
1
+ from typing import Callable
2
+
1
3
  import hypothesis
2
4
  import numpy as np
3
5
  import pytest
4
6
 
5
7
  import linerate
8
+ from linerate.units import Ampere, Celsius, WattPerMeter
6
9
 
7
10
  hypothesis.settings.register_profile("default", deadline=None)
8
11
  hypothesis.settings.load_profile("default")
@@ -86,3 +89,13 @@ def example_model_2_conductors(example_span_2_conductors, example_weather_a):
86
89
  return linerate.Cigre601(
87
90
  example_span_2_conductors, example_weather_a, np.datetime64("2016-06-10 11:00")
88
91
  )
92
+
93
+
94
+ @pytest.fixture
95
+ def heat_balance() -> Callable[[Celsius, Ampere], WattPerMeter]:
96
+ def _heat_balance(conductor_temperature: Celsius, current: Ampere) -> WattPerMeter:
97
+ I = current # noqa: E741
98
+ T = conductor_temperature
99
+ return (I - 100 * T) * (I + 100 * T)
100
+
101
+ return _heat_balance
@@ -131,6 +131,7 @@ def test_solar_declination_scales_correctly_with_day_of_year(day):
131
131
  )
132
132
  def test_solar_declination_scales_with_dates_and_times(when, longitude):
133
133
  omega = ((-12 + when.hour + when.minute / 60 + longitude / 15) % 24) * np.pi / 12
134
+ omega = np.where(omega >= np.pi, omega - 2 * np.pi, omega)
134
135
  when = np.datetime64(when)
135
136
  assert omega == approx(solar_angles.compute_hour_angle_relative_to_noon(when, longitude))
136
137
 
@@ -189,6 +190,7 @@ def test_compute_sin_solar_altitude_for_span(example_span_1_conductor):
189
190
  )
190
191
  sin_H_s = solar_angles.compute_sin_solar_altitude_for_span(example_span_1_conductor, time)
191
192
 
193
+ assert isinstance(sin_H_s, np.ndarray)
192
194
  assert sin_H_s.shape == time.shape
193
195
 
194
196
  assert sin_H_s == approx(
@@ -231,6 +233,7 @@ def test_compute_sin_solar_effective_incidence_angle_for_span(example_span_1_con
231
233
  example_span_1_conductor, time, sin_H_s
232
234
  )
233
235
 
236
+ assert isinstance(sin_eta, np.ndarray)
234
237
  assert sin_eta.shape == time.shape
235
238
 
236
239
  assert sin_eta == approx(
@@ -61,4 +61,6 @@ def test_vectorization(vectorization_indices):
61
61
  for vectorisation_index in vectorization_indices:
62
62
  shape[vectorisation_index] = 2
63
63
  shape[0] = 1
64
+
65
+ assert isinstance(temperature, np.ndarray)
64
66
  assert temperature.shape == tuple(shape)
@@ -1,27 +1,25 @@
1
+ from collections.abc import Callable
2
+ from functools import partial
3
+
1
4
  import numpy as np
2
5
  import pytest
3
6
 
4
7
  import linerate.solver as solver
8
+ from linerate.units import Ampere, Celsius, WattPerMeter
5
9
 
6
10
 
7
- def test_compute_conductor_temperature_computes_correct_temperature():
8
- def heat_balance(conductor_temperature, current):
9
- A = current
10
- T = conductor_temperature
11
- return (A - 100 * T) * (current + 100 * T)
12
-
11
+ def test_compute_conductor_temperature_computes_correct_temperature(
12
+ heat_balance: Callable[[Celsius, Ampere], WattPerMeter],
13
+ ):
13
14
  conductor_temperature = solver.compute_conductor_temperature(
14
15
  heat_balance, current=1500, min_temperature=0, max_temperature=150, tolerance=1e-8
15
16
  )
16
17
  assert conductor_temperature == pytest.approx(15, rel=1e-7)
17
18
 
18
19
 
19
- def test_compute_conductor_ampacity_computes_correct_ampacity():
20
- def heat_balance(conductor_temperature, current):
21
- A = current
22
- T = conductor_temperature
23
- return (A - 100 * T) * (current + 100 * T)
24
-
20
+ def test_compute_conductor_ampacity_computes_correct_ampacity(
21
+ heat_balance: Callable[[Celsius, Ampere], WattPerMeter],
22
+ ):
25
23
  conductor_temperature = solver.compute_conductor_ampacity(
26
24
  heat_balance,
27
25
  max_conductor_temperature=90,
@@ -34,9 +32,9 @@ def test_compute_conductor_ampacity_computes_correct_ampacity():
34
32
 
35
33
  def test_bisect_raises_value_error():
36
34
  def heat_balance(current):
37
- A = current
35
+ I = current # noqa: E741
38
36
  T = 90
39
- return (A + 100 * T) * (A + 100 * T)
37
+ return (I + 100 * T) * (I + 100 * T)
40
38
 
41
39
  with pytest.raises(ValueError):
42
40
  solver.bisect(
@@ -47,15 +45,13 @@ def test_bisect_raises_value_error():
47
45
  )
48
46
 
49
47
 
50
- def test_bisect_handles_function_returning_array_happy_path():
51
- def heat_balance(currents: np.array):
52
- A = currents
53
- T = 90
54
- res = (A - 100 * T) * (currents + 100 * T)
55
- return res
48
+ def test_bisect_handles_function_returning_array_happy_path(
49
+ heat_balance: Callable[[Celsius, Ampere], WattPerMeter],
50
+ ):
51
+ _heat_balance = partial(heat_balance, 90)
56
52
 
57
53
  solution = solver.bisect(
58
- heat_balance,
54
+ _heat_balance,
59
55
  xmin=np.array([0, 0]),
60
56
  xmax=np.array([10_000, 10_000]),
61
57
  tolerance=1e-8,
@@ -63,16 +59,13 @@ def test_bisect_handles_function_returning_array_happy_path():
63
59
  np.testing.assert_array_almost_equal(solution, [9_000, 9_000], decimal=8)
64
60
 
65
61
 
66
- def test_bisect_raises_valueerror_when_same_sign_for_array_input():
67
- def heat_balance(currents: np.array):
68
- A = currents
69
- T = 90
70
- res = (A - 100 * T) * (currents + 100 * T)
71
- return res
72
-
62
+ def test_bisect_raises_valueerror_when_same_sign_for_array_input(
63
+ heat_balance: Callable[[Celsius, Ampere], WattPerMeter],
64
+ ):
65
+ _heat_balance = partial(heat_balance, 90)
73
66
  with pytest.raises(ValueError):
74
67
  solver.bisect(
75
- heat_balance,
68
+ _heat_balance,
76
69
  xmin=np.array([0, 0]),
77
70
  xmax=np.array([10_000, 8_000]),
78
71
  tolerance=1e-8,
@@ -89,27 +82,26 @@ def test_bisect_raises_valueerror_when_infinite_in_array_input():
89
82
  )
90
83
 
91
84
 
92
- def test_bisect_returns_dtype_float_if_invalid_value_is_none():
93
- def heat_balance(currents: np.array):
94
- A = currents
95
- T = 90
96
- res = (A - 100 * T) * (currents + 100 * T)
97
- return res
85
+ def test_bisect_returns_dtype_float_if_not_accept_invalid_values(
86
+ heat_balance: Callable[[Celsius, Ampere], WattPerMeter],
87
+ ):
88
+ _heat_balance = partial(heat_balance, 90)
98
89
 
99
90
  solution = solver.bisect(
100
- heat_balance,
91
+ _heat_balance,
101
92
  xmin=np.array([0, 0]),
102
93
  xmax=np.array([10_000, 10_000]),
103
94
  tolerance=1e-8,
104
- invalid_value=None,
95
+ accept_invalid_values=False,
105
96
  )
106
97
 
98
+ assert isinstance(solution, np.ndarray)
107
99
  assert solution.dtype == np.float64
108
100
 
109
101
 
110
102
  def test_bisect_return_nan_if_heat_balance_returns_nan():
111
- def heat_balance(currents: np.array):
112
- return np.ones_like(currents) * np.nan
103
+ def heat_balance(current: Ampere) -> WattPerMeter:
104
+ return np.ones_like(current) * np.nan
113
105
 
114
106
  solution = solver.bisect(
115
107
  heat_balance,
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 2
3
3
  requires-python = ">=3.9"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.12'",
@@ -842,7 +842,10 @@ dependencies = [
842
842
  dev = [
843
843
  { name = "coverage", extra = ["toml"] },
844
844
  { name = "hypothesis" },
845
+ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
846
+ { name = "matplotlib", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
845
847
  { name = "pre-commit" },
848
+ { name = "pyright", extra = ["nodejs"] },
846
849
  { name = "pytest" },
847
850
  { name = "pytest-cov" },
848
851
  { name = "pytest-randomly" },
@@ -871,7 +874,9 @@ requires-dist = [
871
874
  dev = [
872
875
  { name = "coverage", extras = ["toml"] },
873
876
  { name = "hypothesis", specifier = ">=6.56.2" },
877
+ { name = "matplotlib", specifier = ">=3.9.4" },
874
878
  { name = "pre-commit" },
879
+ { name = "pyright", extras = ["nodejs"] },
875
880
  { name = "pytest", specifier = "==8.4.2" },
876
881
  { name = "pytest-cov", specifier = "==7.0.0" },
877
882
  { name = "pytest-randomly", specifier = "==4.0.1" },
@@ -1146,6 +1151,22 @@ wheels = [
1146
1151
  { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
1147
1152
  ]
1148
1153
 
1154
+ [[package]]
1155
+ name = "nodejs-wheel-binaries"
1156
+ version = "24.13.0"
1157
+ source = { registry = "https://pypi.org/simple" }
1158
+ sdist = { url = "https://files.pythonhosted.org/packages/b7/f1/73182280e2c05f49a7c2c8dbd46144efe3f74f03f798fb90da67b4a93bbf/nodejs_wheel_binaries-24.13.0.tar.gz", hash = "sha256:766aed076e900061b83d3e76ad48bfec32a035ef0d41bd09c55e832eb93ef7a4", size = 8056, upload-time = "2026-01-14T11:05:33.653Z" }
1159
+ wheels = [
1160
+ { url = "https://files.pythonhosted.org/packages/c4/dc/4d7548aa74a5b446d093f03aff4fb236b570959d793f21c9c42ab6ad870a/nodejs_wheel_binaries-24.13.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:356654baa37bfd894e447e7e00268db403ea1d223863963459a0fbcaaa1d9d48", size = 55133268, upload-time = "2026-01-14T11:05:05.335Z" },
1161
+ { url = "https://files.pythonhosted.org/packages/24/8a/8a4454d28339487240dd2232f42f1090e4a58544c581792d427f6239798c/nodejs_wheel_binaries-24.13.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:92fdef7376120e575f8b397789bafcb13bbd22a1b4d21b060d200b14910f22a5", size = 55314800, upload-time = "2026-01-14T11:05:09.121Z" },
1162
+ { url = "https://files.pythonhosted.org/packages/e7/fb/46c600fcc748bd13bc536a735f11532a003b14f5c4dfd6865f5911672175/nodejs_wheel_binaries-24.13.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3f619ac140e039ecd25f2f71d6e83ad1414017a24608531851b7c31dc140cdfd", size = 59666320, upload-time = "2026-01-14T11:05:12.369Z" },
1163
+ { url = "https://files.pythonhosted.org/packages/85/47/d48f11fc5d1541ace5d806c62a45738a1db9ce33e85a06fe4cd3d9ce83f6/nodejs_wheel_binaries-24.13.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:dfb31ebc2c129538192ddb5bedd3d63d6de5d271437cd39ea26bf3fe229ba430", size = 60162447, upload-time = "2026-01-14T11:05:16.003Z" },
1164
+ { url = "https://files.pythonhosted.org/packages/b1/74/d285c579ae8157c925b577dde429543963b845e69cd006549e062d1cf5b6/nodejs_wheel_binaries-24.13.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fdd720d7b378d5bb9b2710457bbc880d4c4d1270a94f13fbe257198ac707f358", size = 61659994, upload-time = "2026-01-14T11:05:19.68Z" },
1165
+ { url = "https://files.pythonhosted.org/packages/ba/97/88b4254a2ff93ed2eaed725f77b7d3d2d8d7973bf134359ce786db894faf/nodejs_wheel_binaries-24.13.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ad6383613f3485a75b054647a09f1cd56d12380d7459184eebcf4a5d403f35c", size = 62244373, upload-time = "2026-01-14T11:05:23.987Z" },
1166
+ { url = "https://files.pythonhosted.org/packages/4e/c3/0e13a3da78f08cb58650971a6957ac7bfef84164b405176e53ab1e3584e2/nodejs_wheel_binaries-24.13.0-py2.py3-none-win_amd64.whl", hash = "sha256:605be4763e3ef427a3385a55da5a1bcf0a659aa2716eebbf23f332926d7e5f23", size = 41345528, upload-time = "2026-01-14T11:05:27.67Z" },
1167
+ { url = "https://files.pythonhosted.org/packages/a3/f1/0578d65b4e3dc572967fd702221ea1f42e1e60accfb6b0dd8d8f15410139/nodejs_wheel_binaries-24.13.0-py2.py3-none-win_arm64.whl", hash = "sha256:2e3431d869d6b2dbeef1d469ad0090babbdcc8baaa72c01dd3cc2c6121c96af5", size = 39054688, upload-time = "2026-01-14T11:05:30.739Z" },
1168
+ ]
1169
+
1149
1170
  [[package]]
1150
1171
  name = "numba"
1151
1172
  version = "0.60.0"
@@ -1545,6 +1566,24 @@ wheels = [
1545
1566
  { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
1546
1567
  ]
1547
1568
 
1569
+ [[package]]
1570
+ name = "pyright"
1571
+ version = "1.1.408"
1572
+ source = { registry = "https://pypi.org/simple" }
1573
+ dependencies = [
1574
+ { name = "nodeenv" },
1575
+ { name = "typing-extensions" },
1576
+ ]
1577
+ sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" }
1578
+ wheels = [
1579
+ { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" },
1580
+ ]
1581
+
1582
+ [package.optional-dependencies]
1583
+ nodejs = [
1584
+ { name = "nodejs-wheel-binaries" },
1585
+ ]
1586
+
1548
1587
  [[package]]
1549
1588
  name = "pytest"
1550
1589
  version = "8.4.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes