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.
- {linerate-2.1.3 → linerate-2.2.1}/.github/workflows/tests.yml +3 -3
- {linerate-2.1.3 → linerate-2.2.1}/.pre-commit-config.yaml +4 -0
- {linerate-2.1.3 → linerate-2.2.1}/PKG-INFO +1 -1
- {linerate-2.1.3 → linerate-2.2.1}/examples/plot_solar_heating_comparison.py +4 -1
- {linerate-2.1.3 → linerate-2.2.1}/examples/plot_solar_radiation.py +14 -11
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/ieee738/convective_cooling.py +1 -1
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/solar_angles.py +12 -6
- {linerate-2.1.3 → linerate-2.2.1}/linerate/models/cigre207.py +1 -1
- {linerate-2.1.3 → linerate-2.2.1}/linerate/models/cigre601.py +50 -35
- {linerate-2.1.3 → linerate-2.2.1}/linerate/models/ieee738.py +2 -4
- {linerate-2.1.3 → linerate-2.2.1}/linerate/models/thermal_model.py +7 -2
- {linerate-2.1.3 → linerate-2.2.1}/linerate/solver.py +18 -15
- {linerate-2.1.3 → linerate-2.2.1}/linerate/types.py +9 -12
- {linerate-2.1.3 → linerate-2.2.1}/linerate/units.py +1 -1
- {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/PKG-INFO +1 -1
- {linerate-2.1.3 → linerate-2.2.1}/pyproject.toml +6 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/conftest.py +13 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_solar_angles.py +3 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/integration_tests/test_vectorization.py +2 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/test_solver.py +31 -39
- {linerate-2.1.3 → linerate-2.2.1}/uv.lock +40 -1
- {linerate-2.1.3 → linerate-2.2.1}/.github/workflows/deploy.yml +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/.gitignore +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/LICENSE +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/README.md +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/Makefile +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/_static/css/custom.css +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/cigre207/convective_cooling.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/cigre207/solar_heating.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/cigre601/convective_cooling.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/cigre601/solar_heating.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/convective_cooling.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/dimensionless.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/ieee738/convective_cooling.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/ieee738/solar_heating.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/index.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/joule_heating.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/math.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/radiative_cooling.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/solar_angles.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/equations/solar_heating.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/model.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/solver.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api/types.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/api.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/bibliography.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/conf.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/figs/frontpage.png +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/index.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/make.bat +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/docs/refs.bib +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/examples/README.rst +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/examples/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/examples/plot_cigre.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre207/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre207/ac_resistance.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre207/convective_cooling.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre207/solar_heating.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre601/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre601/convective_cooling.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre601/py.typed +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/cigre601/solar_heating.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/convective_cooling.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/dimensionless.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/ieee738/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/ieee738/py.typed +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/ieee738/solar_heating.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/joule_heating.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/math.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/py.typed +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/radiative_cooling.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/equations/solar_heating.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/model.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/models/Cigre207.md +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate/py.typed +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/SOURCES.txt +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/dependency_links.txt +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/requires.txt +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/linerate.egg-info/top_level.txt +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/renovate.json +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/setup.cfg +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/test_cigre_ampacity_cases.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/test_ieee_ampacity_cases.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/test_ratekit_ampacity_cases.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/acceptance_tests/test_thermal_model.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/cigre207/test_convective_cooling.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/cigre601/__init__.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/cigre601/test_convective_cooling.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/cigre601/test_solar_heating.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/ieee738/test_ieee_convective_cooling.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/ieee738/test_solar_heating.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_convective_cooling.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_dimensionless.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_joule_heating.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_math.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_radiative_cooling.py +0 -0
- {linerate-2.1.3 → linerate-2.2.1}/tests/equations/test_solar_heating.py +0 -0
- {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
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linerate
|
|
3
|
-
Version: 2.1
|
|
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(
|
|
62
|
+
axes = [fig.add_axes((0.07, 0.27, 0.19, 0.7))]
|
|
61
63
|
axes += [
|
|
62
|
-
fig.add_axes(
|
|
63
|
-
fig.add_axes(
|
|
64
|
-
fig.add_axes(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
else:
|
|
127
130
|
C = pi
|
|
128
131
|
elif 0 <= omega < pi:
|
|
129
132
|
if chi >= 0:
|
|
130
133
|
C = pi
|
|
131
|
-
|
|
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
|
----------
|
|
@@ -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
|
|
24
|
+
class BaseCigre601(ThermalModel):
|
|
26
25
|
def __init__(
|
|
27
26
|
self,
|
|
28
27
|
span: Span,
|
|
29
|
-
weather:
|
|
28
|
+
weather: BaseWeather,
|
|
30
29
|
time: Date,
|
|
31
|
-
max_reynolds_number:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
:math:`\text{sign}(f(\mathbf{x}_\min)) = \text{sign}(f(\mathbf{x}_\max))
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
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(
|
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
35
|
+
I = current # noqa: E741
|
|
38
36
|
T = 90
|
|
39
|
-
return (
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
112
|
-
return np.ones_like(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|