switchgear-thermal-model 0.0.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.
- switchgear_thermal_model-0.0.1/PKG-INFO +121 -0
- switchgear_thermal_model-0.0.1/README.md +104 -0
- switchgear_thermal_model-0.0.1/pyproject.toml +140 -0
- switchgear_thermal_model-0.0.1/switchgear_thermal_model/__init__.py +4 -0
- switchgear_thermal_model-0.0.1/switchgear_thermal_model/schemas/__init__.py +7 -0
- switchgear_thermal_model-0.0.1/switchgear_thermal_model/schemas/input_profile.py +193 -0
- switchgear_thermal_model-0.0.1/switchgear_thermal_model/switchgear.py +45 -0
- switchgear_thermal_model-0.0.1/switchgear_thermal_model/thermal_model.py +54 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: switchgear-thermal-model
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Thermal model for switchgears.
|
|
5
|
+
Author: Contributors to the Switchgear Thermal Model project
|
|
6
|
+
Classifier: Natural Language :: English
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Dist: numpy>=2.2.5,<3
|
|
13
|
+
Requires-Dist: pandas>=2.0.0,<4.0.0
|
|
14
|
+
Requires-Dist: pydantic>=2.12.1,<3
|
|
15
|
+
Requires-Python: >=3.12, <4
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
<!--
|
|
19
|
+
SPDX-FileCopyrightText: Contributors to the Switchgear Thermal Model project
|
|
20
|
+
|
|
21
|
+
SPDX-License-Identifier: MPL-2.0
|
|
22
|
+
-->
|
|
23
|
+
|
|
24
|
+
# Switchgear thermal model
|
|
25
|
+
|
|
26
|
+
[](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/LICENSE)
|
|
27
|
+
|
|
28
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
29
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
30
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
31
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
32
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
33
|
+
|
|
34
|
+
thermal model which can simulate the temperature of the switchgear, based on
|
|
35
|
+
the switchgear specifications, ambient temperature and load profiles.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
One can install the switchgear thermal model from PyPI by running
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install switchgear-thermal-model
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Simple Example
|
|
46
|
+
|
|
47
|
+
The model uses an `InputProfile`, which is a combination of a datetime index,
|
|
48
|
+
load profile, and ambient temperature profile as input for the model.
|
|
49
|
+
A quick start can be made using the following code snippet:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import numpy as np
|
|
53
|
+
|
|
54
|
+
from switchgear_thermal_model.schemas.input_profile import InputProfile
|
|
55
|
+
from switchgear_thermal_model.switchgear import Switchgear
|
|
56
|
+
from switchgear_thermal_model.thermal_model import switchgear_temp
|
|
57
|
+
|
|
58
|
+
# Set up the InputProfile
|
|
59
|
+
start_time = np.datetime64("2024-01-01T00:00")
|
|
60
|
+
dt_minutes = 15
|
|
61
|
+
datetime_array = start_time + np.arange(96) * np.timedelta64(dt_minutes, "m")
|
|
62
|
+
load_array = np.full((1, 96), 2000)[0]
|
|
63
|
+
amb_temp_array = np.full((1, 96), 20)[0]
|
|
64
|
+
|
|
65
|
+
input_profile = InputProfile(
|
|
66
|
+
datetime_index=datetime_array,
|
|
67
|
+
ambient_temperature_profile=amb_temp_array,
|
|
68
|
+
load_profile=load_array,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Set up a switchgear with specifications
|
|
72
|
+
switchgear = Switchgear(
|
|
73
|
+
rated_current=3150,
|
|
74
|
+
measured_temperature_rise=75,
|
|
75
|
+
temp_rise_exp=1.8,
|
|
76
|
+
thermal_time_constant=90
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# run the model
|
|
80
|
+
result = switchgear_temp(input_profile, switchgear)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Features
|
|
84
|
+
|
|
85
|
+
- **Creating switchgear models**: Create a switchgear model based on specified thermal parameters.
|
|
86
|
+
- **Temperature modeling**: Calculate conductor temperature according to IEC 62271‑306.
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
- [Getting Started](https://alliander-opensource.github.io/switchgear-thermal-model/get_started/about/)
|
|
91
|
+
- [API Reference](https://alliander-opensource.github.io/switchgear-thermal-model/api_reference/switchgear_temp/)
|
|
92
|
+
- [Examples](https://alliander-opensource.github.io/switchgear-thermal-model/examples/quick_start/)
|
|
93
|
+
- [Technical Documentation](https://alliander-opensource.github.io/switchgear-thermal-model/theoretical_documentation/model_equations/)
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
This project is licensed under the Mozilla Public License, version 2.0 - see
|
|
98
|
+
[LICENSE](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/LICENSE) for details.
|
|
99
|
+
|
|
100
|
+
## Licenses third-party libraries
|
|
101
|
+
|
|
102
|
+
This project includes third-party libraries,
|
|
103
|
+
which are licensed under their own respective Open-Source licenses.
|
|
104
|
+
SPDX-License-Identifier headers are used to show which license is applicable.
|
|
105
|
+
|
|
106
|
+
An overview of these third-party libraries and their licenses is outlined in [THIRD_PARTY_NOTICES](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/THIRD_PARTY_NOTICES.md)
|
|
107
|
+
|
|
108
|
+
## Contributing
|
|
109
|
+
|
|
110
|
+
Please read
|
|
111
|
+
[CODE_OF_CONDUCT](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/CODE_OF_CONDUCT.md),
|
|
112
|
+
[CONTRIBUTING](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/CONTRIBUTING.md)
|
|
113
|
+
and
|
|
114
|
+
[PROJECT GOVERNANCE](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/GOVERNANCE.md)
|
|
115
|
+
for details on the process
|
|
116
|
+
for submitting pull requests to us.
|
|
117
|
+
|
|
118
|
+
## Contact
|
|
119
|
+
|
|
120
|
+
Please read [SUPPORT](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/SUPPORT.md) for how to
|
|
121
|
+
get in touch with the Switchgear Thermal Model project.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: Contributors to the Switchgear Thermal Model project
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Switchgear thermal model
|
|
8
|
+
|
|
9
|
+
[](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
12
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
13
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
14
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
15
|
+
[](https://sonarcloud.io/summary/new_code?id=alliander-opensource_switchgear-thermal-model)
|
|
16
|
+
|
|
17
|
+
thermal model which can simulate the temperature of the switchgear, based on
|
|
18
|
+
the switchgear specifications, ambient temperature and load profiles.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
One can install the switchgear thermal model from PyPI by running
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install switchgear-thermal-model
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Simple Example
|
|
29
|
+
|
|
30
|
+
The model uses an `InputProfile`, which is a combination of a datetime index,
|
|
31
|
+
load profile, and ambient temperature profile as input for the model.
|
|
32
|
+
A quick start can be made using the following code snippet:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
from switchgear_thermal_model.schemas.input_profile import InputProfile
|
|
38
|
+
from switchgear_thermal_model.switchgear import Switchgear
|
|
39
|
+
from switchgear_thermal_model.thermal_model import switchgear_temp
|
|
40
|
+
|
|
41
|
+
# Set up the InputProfile
|
|
42
|
+
start_time = np.datetime64("2024-01-01T00:00")
|
|
43
|
+
dt_minutes = 15
|
|
44
|
+
datetime_array = start_time + np.arange(96) * np.timedelta64(dt_minutes, "m")
|
|
45
|
+
load_array = np.full((1, 96), 2000)[0]
|
|
46
|
+
amb_temp_array = np.full((1, 96), 20)[0]
|
|
47
|
+
|
|
48
|
+
input_profile = InputProfile(
|
|
49
|
+
datetime_index=datetime_array,
|
|
50
|
+
ambient_temperature_profile=amb_temp_array,
|
|
51
|
+
load_profile=load_array,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Set up a switchgear with specifications
|
|
55
|
+
switchgear = Switchgear(
|
|
56
|
+
rated_current=3150,
|
|
57
|
+
measured_temperature_rise=75,
|
|
58
|
+
temp_rise_exp=1.8,
|
|
59
|
+
thermal_time_constant=90
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# run the model
|
|
63
|
+
result = switchgear_temp(input_profile, switchgear)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
- **Creating switchgear models**: Create a switchgear model based on specified thermal parameters.
|
|
69
|
+
- **Temperature modeling**: Calculate conductor temperature according to IEC 62271‑306.
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
- [Getting Started](https://alliander-opensource.github.io/switchgear-thermal-model/get_started/about/)
|
|
74
|
+
- [API Reference](https://alliander-opensource.github.io/switchgear-thermal-model/api_reference/switchgear_temp/)
|
|
75
|
+
- [Examples](https://alliander-opensource.github.io/switchgear-thermal-model/examples/quick_start/)
|
|
76
|
+
- [Technical Documentation](https://alliander-opensource.github.io/switchgear-thermal-model/theoretical_documentation/model_equations/)
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
This project is licensed under the Mozilla Public License, version 2.0 - see
|
|
81
|
+
[LICENSE](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/LICENSE) for details.
|
|
82
|
+
|
|
83
|
+
## Licenses third-party libraries
|
|
84
|
+
|
|
85
|
+
This project includes third-party libraries,
|
|
86
|
+
which are licensed under their own respective Open-Source licenses.
|
|
87
|
+
SPDX-License-Identifier headers are used to show which license is applicable.
|
|
88
|
+
|
|
89
|
+
An overview of these third-party libraries and their licenses is outlined in [THIRD_PARTY_NOTICES](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/THIRD_PARTY_NOTICES.md)
|
|
90
|
+
|
|
91
|
+
## Contributing
|
|
92
|
+
|
|
93
|
+
Please read
|
|
94
|
+
[CODE_OF_CONDUCT](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/CODE_OF_CONDUCT.md),
|
|
95
|
+
[CONTRIBUTING](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/CONTRIBUTING.md)
|
|
96
|
+
and
|
|
97
|
+
[PROJECT GOVERNANCE](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/GOVERNANCE.md)
|
|
98
|
+
for details on the process
|
|
99
|
+
for submitting pull requests to us.
|
|
100
|
+
|
|
101
|
+
## Contact
|
|
102
|
+
|
|
103
|
+
Please read [SUPPORT](https://github.com/alliander-opensource/switchgear-thermal-model/blob/main/SUPPORT.md) for how to
|
|
104
|
+
get in touch with the Switchgear Thermal Model project.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Switchgear Thermal Model project
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "switchgear-thermal-model"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Thermal model for switchgears."
|
|
9
|
+
authors = [{ name = "Contributors to the Switchgear Thermal Model project" }]
|
|
10
|
+
requires-python = ">=3.12,<4"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Natural Language :: English",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"numpy>=2.2.5,<3",
|
|
22
|
+
"pandas>=2.0.0, <4.0.0",
|
|
23
|
+
"pydantic>=2.12.1,<3",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"ruff>=0.11.9,<0.16.0",
|
|
29
|
+
"mypy>=2.0.0,<3",
|
|
30
|
+
"pytest-cov>=7.1.0,<8",
|
|
31
|
+
"prek>=0.4.9",
|
|
32
|
+
"codespell>=2.4.1,<3",
|
|
33
|
+
"pytest>=9.0.3,<10",
|
|
34
|
+
"ipykernel>=7.0.1,<8",
|
|
35
|
+
]
|
|
36
|
+
docs = [
|
|
37
|
+
"mkdocs>=1.6.1,<2",
|
|
38
|
+
"mkdocstrings-python>=2.0.0,<3",
|
|
39
|
+
"mkdocs-exclude==1.0.0",
|
|
40
|
+
"mkdocs-llmstxt>=0.5.0,<0.6",
|
|
41
|
+
"mkdocs-material>=9.7.6,<10",
|
|
42
|
+
"mkdocs-mermaid2-plugin>=1.2.1,<2",
|
|
43
|
+
"mkdocs-jupyter>=0.26.3,<0.27",
|
|
44
|
+
"matplotlib>=3.11.1,<4",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.uv]
|
|
48
|
+
exclude-newer = "7 days"
|
|
49
|
+
|
|
50
|
+
[tool.uv.build-backend]
|
|
51
|
+
module-name = "switchgear_thermal_model"
|
|
52
|
+
module-root = "."
|
|
53
|
+
|
|
54
|
+
[build-system]
|
|
55
|
+
requires = ["uv_build>=0.11.21,<0.12.0"]
|
|
56
|
+
build-backend = "uv_build"
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
output-format = "github"
|
|
60
|
+
line-length = 120
|
|
61
|
+
show-fixes = true
|
|
62
|
+
exclude = [
|
|
63
|
+
".bzr",
|
|
64
|
+
".direnv",
|
|
65
|
+
".eggs",
|
|
66
|
+
".git",
|
|
67
|
+
".git-rewrite",
|
|
68
|
+
".hg",
|
|
69
|
+
".ipynb_checkpoints",
|
|
70
|
+
".mypy_cache",
|
|
71
|
+
".nox",
|
|
72
|
+
".pants.d",
|
|
73
|
+
".pyenv",
|
|
74
|
+
".pytest_cache",
|
|
75
|
+
".pytype",
|
|
76
|
+
".ruff_cache",
|
|
77
|
+
".svn",
|
|
78
|
+
".tox",
|
|
79
|
+
".venv",
|
|
80
|
+
".vscode",
|
|
81
|
+
"__pypackages__",
|
|
82
|
+
"_build",
|
|
83
|
+
"buck-out",
|
|
84
|
+
"build",
|
|
85
|
+
"dist",
|
|
86
|
+
"node_modules",
|
|
87
|
+
"site-packages",
|
|
88
|
+
"venv",
|
|
89
|
+
"migrations",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint]
|
|
93
|
+
select = [
|
|
94
|
+
# pycodestyle
|
|
95
|
+
"E",
|
|
96
|
+
# Pyflake
|
|
97
|
+
"F",
|
|
98
|
+
# pyupgrade
|
|
99
|
+
"UP",
|
|
100
|
+
# flake8-bugbear
|
|
101
|
+
"B",
|
|
102
|
+
# flake8-simplify
|
|
103
|
+
"SIM",
|
|
104
|
+
# isort
|
|
105
|
+
"I",
|
|
106
|
+
# pydocstyle
|
|
107
|
+
"D",
|
|
108
|
+
]
|
|
109
|
+
ignore = [
|
|
110
|
+
# Missing docstring in public module
|
|
111
|
+
"D100",
|
|
112
|
+
# Missing docstring in public package
|
|
113
|
+
"D104",
|
|
114
|
+
# `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
|
|
115
|
+
"D203",
|
|
116
|
+
# `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
|
|
117
|
+
"D213",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
[tool.pytest.ini_options]
|
|
121
|
+
markers = ["integrationtest"]
|
|
122
|
+
addopts = [
|
|
123
|
+
"--cov",
|
|
124
|
+
"--cov-branch",
|
|
125
|
+
"--cov-report=xml",
|
|
126
|
+
"--cov-report=term-missing" # Coverage report incl missing lines in terminal
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
[tool.coverage.run]
|
|
130
|
+
relative_files = true
|
|
131
|
+
omit = [
|
|
132
|
+
"tests/*"
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
[tool.ruff.lint.pydocstyle]
|
|
136
|
+
convention = "google"
|
|
137
|
+
|
|
138
|
+
[tool.ruff.lint.per-file-ignores]
|
|
139
|
+
# Allow longer line length in conftest where many 1-line dictinaries are specified
|
|
140
|
+
"conftest.py" = ["E501"]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Transformer Thermal Model project
|
|
2
|
+
# SPDX-FileCopyrightText: Contributors to the Switchgear Thermal Model project
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
#
|
|
6
|
+
# Modified from the Transformer Thermal Model project for use in the Switchgear Model project.
|
|
7
|
+
import logging
|
|
8
|
+
from collections.abc import Collection
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Self
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_NEGATIVE_LOAD_PROFILE_ERROR_MESSAGE = "The load profile must not contain negative values"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InputProfile(BaseModel):
|
|
22
|
+
"""Class containing the temperature and load profiles for the switchgear thermal model `Model()`.
|
|
23
|
+
|
|
24
|
+
This class is also capable of converting the results to a single dataframe with the timestamp as the index for
|
|
25
|
+
convenience.
|
|
26
|
+
|
|
27
|
+
This class is a simplified version of the transformer_thermal_model input_profile class (TTM 0.2.1).
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
datetime_index: A 1d array with the datetime index for the profiles.
|
|
31
|
+
load_profile: A 1d array with the load profile for the switchgear.
|
|
32
|
+
ambient_temperature_profile: A 1d array with the ambient temperature profile for the switchgear.
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
datetime_index: np.typing.NDArray[np.datetime64]
|
|
37
|
+
load_profile: np.typing.NDArray[np.float64]
|
|
38
|
+
ambient_temperature_profile: np.typing.NDArray[np.float64]
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
41
|
+
|
|
42
|
+
@model_validator(mode="after")
|
|
43
|
+
def _check_datetime_index_is_sorted(self) -> Self:
|
|
44
|
+
"""Check if the datetime index is sorted."""
|
|
45
|
+
if not np.all(self.datetime_index[:-1] <= self.datetime_index[1:]):
|
|
46
|
+
raise ValueError("The datetime index should be sorted.")
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
@model_validator(mode="after")
|
|
50
|
+
def _check_arrays_are_one_dimensional(self) -> Self:
|
|
51
|
+
"""Check if the arrays are one-dimensional."""
|
|
52
|
+
if self.datetime_index.ndim != 1:
|
|
53
|
+
raise ValueError("The datetime_index array must be one-dimensional.")
|
|
54
|
+
if self.ambient_temperature_profile.ndim != 1:
|
|
55
|
+
raise ValueError("The ambient_temperature_profile array must be one-dimensional.")
|
|
56
|
+
if self.load_profile.ndim != 1:
|
|
57
|
+
raise ValueError("The load_profile array must be one-dimensional.")
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
@model_validator(mode="after")
|
|
61
|
+
def _check_same_length_of_profiles(self) -> Self:
|
|
62
|
+
"""Check if the length of the profiles is the same."""
|
|
63
|
+
if len(self.datetime_index) != len(self.ambient_temperature_profile):
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"The length of the profiles and index should be the same. Index length: {len(self.datetime_index)}, "
|
|
66
|
+
f"ambient temperature profile length: {len(self.ambient_temperature_profile)}"
|
|
67
|
+
)
|
|
68
|
+
if len(self.datetime_index) != len(self.load_profile):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"The length of the profiles and index should be the same. Index length: {len(self.datetime_index)}, "
|
|
71
|
+
f"load profile length: {len(self.load_profile)}"
|
|
72
|
+
)
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
@model_validator(mode="after")
|
|
76
|
+
def _check_load_profile_not_negative(self) -> Self:
|
|
77
|
+
"""Check if the load profile contains negative values."""
|
|
78
|
+
if np.min(self.load_profile_array) < 0:
|
|
79
|
+
raise ValueError(_NEGATIVE_LOAD_PROFILE_ERROR_MESSAGE)
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def create(
|
|
84
|
+
cls,
|
|
85
|
+
datetime_index: Collection[datetime],
|
|
86
|
+
load_profile: Collection[float],
|
|
87
|
+
ambient_temperature_profile: Collection[float],
|
|
88
|
+
) -> Self:
|
|
89
|
+
"""Create an InputProfile.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
datetime_index: The datetime index for the profiles.
|
|
93
|
+
load_profile: The load profile for the switchgear.
|
|
94
|
+
ambient_temperature_profile: The ambient temperature profile for the switchgear.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
An InputProfile object.
|
|
98
|
+
|
|
99
|
+
Example: Creating an InputProfile from collections.
|
|
100
|
+
```python
|
|
101
|
+
>>> from datetime import datetime
|
|
102
|
+
>>> from switchgear_thermal_model.schemas import InputProfile
|
|
103
|
+
|
|
104
|
+
>>> datetime_index = [
|
|
105
|
+
... datetime(2023, 1, 1, 0, 0),
|
|
106
|
+
... datetime(2023, 1, 1, 1, 0),
|
|
107
|
+
... datetime(2023, 1, 1, 2, 0),
|
|
108
|
+
... ]
|
|
109
|
+
>>> load_profile = [0.8, 0.9, 1.0]
|
|
110
|
+
>>> ambient_temperature_profile = [25.0, 24.5, 24.0]
|
|
111
|
+
>>> input_profile = InputProfile.create(
|
|
112
|
+
... datetime_index=datetime_index,
|
|
113
|
+
... load_profile=load_profile,
|
|
114
|
+
... ambient_temperature_profile=ambient_temperature_profile,
|
|
115
|
+
... )
|
|
116
|
+
>>> input_profile
|
|
117
|
+
InputProfile(datetime_index=array(['2023-01-01T00:00:00.000000',
|
|
118
|
+
'2023-01-01T01:00:00.000000', '2023-01-01T02:00:00.000000'],
|
|
119
|
+
dtype='datetime64[us]'), ambient_temperature_profile=array([25. , 24.5, 24. ]),
|
|
120
|
+
load_profile=array([0.8, 0.9, 1. ]))
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Example: Directly creating an InputProfile object using numpy arrays.
|
|
125
|
+
```python
|
|
126
|
+
>>> import numpy as np
|
|
127
|
+
>>> from datetime import datetime
|
|
128
|
+
>>> from switchgear_thermal_model.schemas import InputProfile
|
|
129
|
+
|
|
130
|
+
>>> input_profile = InputProfile(
|
|
131
|
+
... datetime_index=np.array(
|
|
132
|
+
... [
|
|
133
|
+
... datetime(2023, 1, 1, 0, 0),
|
|
134
|
+
... datetime(2023, 1, 1, 1, 0),
|
|
135
|
+
... datetime(2023, 1, 1, 2, 0)
|
|
136
|
+
... ],
|
|
137
|
+
... dtype=np.datetime64,
|
|
138
|
+
... ),
|
|
139
|
+
... load_profile=np.array([0.8, 0.9, 1.0], dtype=float),
|
|
140
|
+
... ambient_temperature_profile=np.array([25.0, 24.5, 24.0], dtype=float)
|
|
141
|
+
... )
|
|
142
|
+
>>> input_profile
|
|
143
|
+
InputProfile(datetime_index=array(['2023-01-01T00:00:00.000000',
|
|
144
|
+
'2023-01-01T01:00:00.000000', '2023-01-01T02:00:00.000000'],
|
|
145
|
+
dtype='datetime64[us]'), ambient_temperature_profile=array([25. , 24.5, 24. ]),
|
|
146
|
+
load_profile=array([0.8, 0.9, 1. ]))
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
"""
|
|
150
|
+
return cls(
|
|
151
|
+
datetime_index=np.array(datetime_index, dtype=np.datetime64),
|
|
152
|
+
load_profile=np.array(load_profile, dtype=float),
|
|
153
|
+
ambient_temperature_profile=np.array(ambient_temperature_profile, dtype=float),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dataframe(cls, df: pd.DataFrame) -> Self:
|
|
158
|
+
"""Create an InputProfile from a dataframe."""
|
|
159
|
+
required_columns = {"datetime_index", "load_profile", "ambient_temperature_profile"}
|
|
160
|
+
missing_columns = required_columns - set(df.columns)
|
|
161
|
+
if missing_columns:
|
|
162
|
+
raise ValueError(f"The dataframe is missing the following required columns: {', '.join(missing_columns)}")
|
|
163
|
+
|
|
164
|
+
return cls(
|
|
165
|
+
datetime_index=df["datetime_index"].to_numpy(),
|
|
166
|
+
load_profile=df["load_profile"].to_numpy(),
|
|
167
|
+
ambient_temperature_profile=df["ambient_temperature_profile"].to_numpy(),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def load_profile_array(self) -> np.typing.NDArray[np.float64]:
|
|
172
|
+
"""Return the single load profile for the switchgear."""
|
|
173
|
+
return self.load_profile
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def dt(self) -> np.ndarray:
|
|
177
|
+
"""The time step between the data points in minutes, with zero for the first element.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
np.ndarray[np.timedelta64]: The time step between the data points in minutes.
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
return np.diff(self.datetime_index, prepend=self.datetime_index[0]).astype("timedelta64[m]").astype(float)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def ambient_temperature_is_constant(self) -> bool:
|
|
187
|
+
"""Check if the ambient temperature profile is constant up to a tolerance of 1e-6."""
|
|
188
|
+
tolerance = 1e-6
|
|
189
|
+
return self.ambient_temperature_profile.max() - self.ambient_temperature_profile.min() <= tolerance
|
|
190
|
+
|
|
191
|
+
def __len__(self) -> int:
|
|
192
|
+
"""Return the length of the datetime index."""
|
|
193
|
+
return len(self.datetime_index)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Switchgear Thermal Model project
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Switchgear(BaseModel):
|
|
9
|
+
"""A switchgear to model the thermal behavior of a switchgear component."""
|
|
10
|
+
|
|
11
|
+
rated_current: float = Field(..., description="Rated current of the switchgear in Amperes (A).")
|
|
12
|
+
measured_temperature_rise: float = Field(
|
|
13
|
+
...,
|
|
14
|
+
description=(
|
|
15
|
+
"Measured temperature rise of the switchgear in Kelvin (K). "
|
|
16
|
+
"How many degrees the switchgear temperature rises above the ambient temperature "
|
|
17
|
+
"when operating its rated current."
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
temp_rise_exp: float = Field(
|
|
21
|
+
...,
|
|
22
|
+
description=(
|
|
23
|
+
"Temperature rise exponent for the switchgear. Determines how the temperature rise scales with the load. "
|
|
24
|
+
"A value of 1.0 indicates a linear relationship, while values greater than 1.0 indicate a nonlinear "
|
|
25
|
+
"relationship (e.g., quadratic or cubic)."
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
thermal_time_constant: float = Field(
|
|
29
|
+
...,
|
|
30
|
+
description=(
|
|
31
|
+
"Thermal time constant of the switchgear in minutes (m). "
|
|
32
|
+
"This determines how quickly the switchgear temperature responds to changes in load."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, rated_current: float, measured_temperature_rise: float, temp_rise_exp: float, thermal_time_constant: float
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the switchgear with its thermal parameters."""
|
|
40
|
+
super().__init__(
|
|
41
|
+
rated_current=rated_current,
|
|
42
|
+
measured_temperature_rise=measured_temperature_rise,
|
|
43
|
+
temp_rise_exp=temp_rise_exp,
|
|
44
|
+
thermal_time_constant=thermal_time_constant,
|
|
45
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Switchgear Thermal Model project
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from switchgear_thermal_model.schemas.input_profile import InputProfile
|
|
9
|
+
from switchgear_thermal_model.switchgear import Switchgear
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def switchgear_temp(data: InputProfile, switchgear: Switchgear) -> pd.Series:
|
|
13
|
+
"""Simulate the temperature profile of a circuit breaker component.
|
|
14
|
+
|
|
15
|
+
This function simulates the temperature profile of the most critical thermal
|
|
16
|
+
point of a certain circuit breaker for a specific ambient temperature
|
|
17
|
+
profile, load profile and circuit breaker specifications. Since the
|
|
18
|
+
switchgears still follow a minimal thermal model, the solution for any
|
|
19
|
+
component in the switchgears can be modeled similarly. Hence, the thermal
|
|
20
|
+
profile can be modeled for a circuit breaker, or disconnector. It just
|
|
21
|
+
depends on the provided specifications. In particular, the measured
|
|
22
|
+
temperature rise.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
data (InputProfile): input profile containing the ambient temperature
|
|
26
|
+
profile and load profile, coupled to a datetime index.
|
|
27
|
+
switchgear (Switchgear): Switchgear object containing the rated current,
|
|
28
|
+
measured temperature rise, temperature rise exponent, and thermal time
|
|
29
|
+
constant.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
np.ndarray
|
|
33
|
+
array of modeled temperatures for the circuit breaker at each timestep
|
|
34
|
+
in the ambient temperature profile
|
|
35
|
+
"""
|
|
36
|
+
initial_temperature = data.ambient_temperature_profile[0]
|
|
37
|
+
modeled_temperature = np.zeros(len(data.ambient_temperature_profile))
|
|
38
|
+
|
|
39
|
+
modeled_temperature[0] = initial_temperature
|
|
40
|
+
normalised_load = data.load_profile / switchgear.rated_current
|
|
41
|
+
temperature_asymptote = (
|
|
42
|
+
switchgear.measured_temperature_rise * (normalised_load**switchgear.temp_rise_exp)
|
|
43
|
+
+ data.ambient_temperature_profile
|
|
44
|
+
)
|
|
45
|
+
exponent = 1 - np.exp(-data.dt / switchgear.thermal_time_constant)
|
|
46
|
+
|
|
47
|
+
for i in range(1, len(data.ambient_temperature_profile)):
|
|
48
|
+
new_temperature = (temperature_asymptote[i] - modeled_temperature[i - 1]) * exponent[i] + modeled_temperature[
|
|
49
|
+
i - 1
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
modeled_temperature[i] = new_temperature
|
|
53
|
+
|
|
54
|
+
return pd.Series(modeled_temperature, index=data.datetime_index)
|