diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3c57805..16c4ede7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,9 @@ -exclude: ^.idea/|^conda/meta.yaml +exclude: ^.idea/|.vscode/|^conda/meta.yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-json - exclude: ^.vscode/ - id: check-merge-conflict - id: check-toml - id: check-yaml @@ -35,4 +34,3 @@ repos: hooks: - id: prettier args: ["--print-width", "120"] - exclude: ^.vscode/ diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 62c34947..756d6369 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -7,6 +7,7 @@ "words": [ "abcn", "absolufy", + "acsr", "asarray", "astype", "bysource", @@ -34,10 +35,13 @@ "susceptance", "transfo", "ureg", + "xlpe", "yesqa" ], // flagWords - list of words to be always considered incorrect // This is useful for offensive words and common spelling errors. // For example "hte" should be "the" - "flagWords": ["hte"] + "flagWords": [ + "hte" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index da5cade4..4ad513a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,9 @@ "[markdown][yaml][html][css]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, - } + }, + // Json + "[json]": { + "editor.indentSize": 2, + }, } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 48ba0e67..689358f5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,7 +5,7 @@ "options": { "cwd": "${workspaceFolder}" }, "presentation": { "showReuseMessage": true, - "clear": true + "clear": true, }, "tasks": [ { @@ -15,13 +15,13 @@ "command": "make -C doc html", "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "problemMatcher": [], "presentation": { "reveal": "silent", - "focus": true - } + "focus": true, + }, }, { "label": "Open docs", @@ -32,13 +32,13 @@ "reveal": "never", "close": true, "focus": false, - "panel": "dedicated" + "panel": "dedicated", }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, - "isBackground": true - } - ] + "isBackground": true, + }, + ], } diff --git a/conda/environment.yml b/conda/environment.yml index efa95a45..dbac36a5 100644 --- a/conda/environment.yml +++ b/conda/environment.yml @@ -11,7 +11,6 @@ dependencies: - regex >=2022.1.18 - pint >=0.21.0 - typing_extensions >=4.6.2 - - rich >=13.5.1 - pyproj >=3.3.0 - matplotlib-base >=3.7.2 - networkx >=3.0.0 diff --git a/conda/meta.yaml b/conda/meta.yaml index 9bebd633..3bc3230d 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -33,7 +33,6 @@ requirements: - regex >=2022.1.18 - pint >=0.21.0 - typing_extensions >=4.6.2 - - rich >=13.5.1 - pyproj >=3.3.0 - matplotlib-base >=3.7.2 - networkx >=3.0.0 diff --git a/doc/conf.py b/doc/conf.py index c591b375..468be0e1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -128,7 +128,6 @@ "geopandas": ("https://geopandas.org/en/stable/", None), "pint": ("https://pint.readthedocs.io/en/stable/", None), "typing_extensions": ("https://typing-extensions.readthedocs.io/en/stable/", None), - "rich": ("https://rich.readthedocs.io/en/stable/", None), "matplotlib": ("https://matplotlib.org/stable/", None), "networkx": ("https://networkx.org/documentation/stable/", None), } diff --git a/doc/models/Line/Parameters.md b/doc/models/Line/Parameters.md index fa3496b0..c8222e2b 100644 --- a/doc/models/Line/Parameters.md +++ b/doc/models/Line/Parameters.md @@ -2,8 +2,15 @@ # Parameters -The line parameters are briefly described [here](models-line_parameters). In this page, the alternative constructors -of `LineParameters` objects are detailed. +As described [in the previous page](models-line_parameters), a line parameters object contains the +impedance and shunt admittance matrices representing the line model. Sometimes you do not have +these matrices available but you have other data such as symmetric components or geometric +configurations and material types. + +This page describes how to build the impedance and shunt admittance matrices and thus the line +parameters object using these alternative data. This is achieved via the alternative constructors +of the `LineParameters` class. Note that only 3-phase lines are supported by the alternative +constructors. (models-line_parameters-alternative_constructors-symmetric)= @@ -11,16 +18,16 @@ of `LineParameters` objects are detailed. ### Definition -The `LineParameters` class has a class method called `from_sym` which converts zero and direct sequences of -impedance and admittance into a line parameters instance. This method requires the following data: +Line parameters can be built from a symmetric model of the line using the `LineParameters.from_sym` +class method. This method takes the following data: - The zero sequence of the impedance (in $\Omega$/km), noted $\underline{Z_0}$ and `z0` in the code. - The direct sequence of the impedance (in $\Omega$/km), noted $\underline{Z_1}$ and `z1` in the code. - The zero sequence of the admittance (in S/km), noted $\underline{Y_0}$ and `y0` in the code. - The direct sequence of the admittance (in S/km), noted $\underline{Y_1}$ and `y1` in the code. -Then, it combines them in order to build the series impedance matrix $\underline{Z}$ and the shunt admittance matrix -$\underline{Y}$ using the following equations: +The symmetric componenets are then used to build the series impedance matrix $\underline{Z}$ and +the shunt admittance matrix $\underline{Y}$ using the following equations: ```{math} \begin{aligned} @@ -51,8 +58,7 @@ defined as: \end{aligned} ``` -This class method also takes optional parameters which are used to add a neutral wire to the previously seen -three-phase matrices. These optional parameters are: +For lines with a neutral, this method also takes the following optional extra parameters: - The neutral impedance (in $\Omega$/km), noted $\underline{Z_{\mathrm{n}}}$ and `zn` in the code. - The phase-to-neutral reactance (in $\Omega$/km), noted $\left(\underline{X_{p\mathrm{n}}}\right)_{p\in\{\mathrm{a}, @@ -63,8 +69,9 @@ three-phase matrices. These optional parameters are: \mathrm{b},\mathrm{c}\}}$. As these are supposed to be the same, this unique value is noted `bpn` in the code. ```{note} -If any of those parameters is omitted, the neutral wire is omitted and a 3 phase line parameters is built. -If $\underline{Z_{\mathrm{n}}}$ and $\underline{X_{p\mathrm{n}}}$ are zeros, the same happens. +If any of those parameters is omitted or if $\underline{Z_{\mathrm{n}}}$ and +$\underline{X_{p\mathrm{n}}}$ are zeros, the neutral wire is omitted and a 3-phase line parameters +is built. ``` In this case, the following matrices are built: @@ -102,8 +109,8 @@ respectively the phase-to-neutral series impedance (in $\Omega$/km), the neutral the phase-to-neutral shunt admittance (in S/km). ````{note} -The computed impedance matrix may be non-invertible. In this case, the `from_sym` class method builds impedance and -shunt admittance matrices using the following definitions: +If the computed impedance matrix is be non-invertible, the `from_sym` class method builds impedance +and shunt admittance matrices using the following definitions: ```{math} \begin{aligned} @@ -204,7 +211,7 @@ matrices from dimensions and materials used for the insulator and the conductors proposed: the first one is for a twisted line and the second is for an underground line. Both of them include a neutral wire. -This class methods accepts the following arguments: +This class method accepts the following arguments: - the line type to choose between the twisted and the underground options. - the conductor type which defines the material of the conductors. @@ -231,15 +238,15 @@ where: The following resistivities are used by _Roseau Load Flow_: -| Material | Resistivity ($\Omega$m) | -| :------------ | :---------------------- | -| Copper | $1.72\times10^{-8}$ | -| Aluminium | $2.82\times10^{-8}$ | -| Almélec | $3.26\times10^{-8}$ | -| Alu-Acier | $4.0587\times10^{-8}$ | -| Almélec-Acier | $3.26\times10^{-8}$ | +| Material | Resistivity ($\Omega$m) | +| :------------------------- | :---------------------- | +| Copper -- Fr: Cuivre | $1.72\times10^{-8}$ | +| Aluminum -- Fr: Aluminium | $2.82\times10^{-8}$ | +| Al-Mg Alloy -- Fr: Almélec | $3.26\times10^{-8}$ | +| ACSR -- Fr: Alu-Acier | $4.0587\times10^{-8}$ | +| AACSR -- Fr: Almélec-Acier | $3.26\times10^{-8}$ | -These values are defined in the `utils` module: [](#roseau.load_flow.utils.constants.RHO). +These values are defined in the `utils` module: {data}`roseau.load_flow.utils.constants.RHO`. #### Inductance @@ -271,7 +278,7 @@ where: - $D_{ij}$ the distances between the center of the conductor $i$ and the center of the conductor $j$ - $GMR_i$ the _geometric mean radius_ of the conductor $i$. -The vacuum magnetic permeability is defined in the `utils` module [](#roseau.load_flow.utils.constants.MU_0). +The vacuum magnetic permeability is defined in the `utils` module {data}`roseau.load_flow.utils.constants.MU_0`. The geometric mean radius is defined for all $i\in \{\mathrm{a}, \mathrm{b}, \mathrm{c}, \mathrm{n}\}$ as diff --git a/doc/models/Transformer/Center_Tapped_Transformer.md b/doc/models/Transformer/Center_Tapped_Transformer.md index d8092f11..fed2ff3a 100644 --- a/doc/models/Transformer/Center_Tapped_Transformer.md +++ b/doc/models/Transformer/Center_Tapped_Transformer.md @@ -95,7 +95,9 @@ load_bus = Bus(id="load_bus", phases="abc") mv_load = PowerLoad("mv_load", load_bus, powers=[10000, 10000, 10000]) # Connect the two MV buses with a line -lp = LineParameters.from_name_mv("U_AL_150") # Underground, ALuminium, 150mm² +lp = LineParameters.from_catalogue( + id="U_AL_150", model="iec" +) # Underground, ALuminium, 150mm² line = Line("line", source_bus, load_bus, parameters=lp, length=1.0, ground=ground) # Create a low-voltage bus and a load diff --git a/doc/usage/Catalogues.md b/doc/usage/Catalogues.md index 976ee42f..65ca4c13 100644 --- a/doc/usage/Catalogues.md +++ b/doc/usage/Catalogues.md @@ -19,60 +19,57 @@ interactive map. All these networks are built from open data available in France. The entire France can be provided on demand. Please email us at [contact@roseautechnologies.com](mailto:contact@roseautechnologies.com). -### Printing the catalogue +### Inspecting the catalogue -This catalogue can be printed to the terminal: +This catalogue can be retrieved in the form of a dataframe using: ```pycon >>> from roseau.load_flow import ElectricalNetwork ->>> ElectricalNetwork.print_catalogue() +>>> ElectricalNetwork.get_catalogue() ``` | Name | Nb buses | Nb branches | Nb loads | Nb sources | Nb grounds | Nb potential refs | Available load points | -| :-------------------------------------------------------------------------------- | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | --------------------: | -| LVFeeder00939 | 8 | 7 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder02639 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder04790 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06713 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06926 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06975 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder18498 | 18 | 17 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder18769 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder19558 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder20256 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder23832 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder24400 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder27429 | 11 | 10 | 18 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder27681 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder30216 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder31441 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder36284 | 5 | 4 | 6 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder36360 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder37263 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder004 | 17 | 16 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder011 | 50 | 49 | 68 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder015 | 30 | 29 | 20 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder032 | 53 | 52 | 40 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder041 | 88 | 87 | 62 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder063 | 39 | 38 | 38 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder078 | 69 | 68 | 46 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder115 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder128 | 49 | 48 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder151 | 59 | 58 | 44 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder159 | 8 | 7 | 0 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder176 | 33 | 32 | 20 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder210 | 128 | 127 | 82 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder217 | 44 | 43 | 44 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder232 | 66 | 65 | 38 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder251 | 125 | 124 | 106 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder290 | 12 | 11 | 16 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder312 | 11 | 10 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder320 | 20 | 19 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder339 | 33 | 32 | 28 | 1 | 1 | 1 | 'Summer', 'Winter' | - -The table is printed using the [Rich Python library](https://rich.readthedocs.io/en/stable/index.html). Links to the -map of each network have been added in the documentation. +| :-------------------------------------------------------------------------------- | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | :-------------------- | +| LVFeeder00939 | 8 | 7 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder02639 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder04790 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06713 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06926 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06975 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder18498 | 18 | 17 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder18769 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder19558 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder20256 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder23832 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder24400 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder27429 | 11 | 10 | 18 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder27681 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder30216 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder31441 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder36284 | 5 | 4 | 6 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder36360 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder37263 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder004 | 17 | 16 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder011 | 50 | 49 | 68 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder015 | 30 | 29 | 20 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder032 | 53 | 52 | 40 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder041 | 88 | 87 | 62 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder063 | 39 | 38 | 38 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder078 | 69 | 68 | 46 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder115 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder128 | 49 | 48 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder151 | 59 | 58 | 44 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder159 | 8 | 7 | 0 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder176 | 33 | 32 | 20 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder210 | 128 | 127 | 82 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder217 | 44 | 43 | 44 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder232 | 66 | 65 | 38 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder251 | 125 | 124 | 106 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder290 | 12 | 11 | 16 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder312 | 11 | 10 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder320 | 20 | 19 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder339 | 33 | 32 | 28 | 1 | 1 | 1 | 'Summer', 'Winter' | There are MV networks whose names start with "MVFeeder" and LV networks whose names with "LVFeeder". For each network, there are two available load points: @@ -80,62 +77,62 @@ network, there are two available load points: - "Winter": it contains power loads without production. - "Summer": it contains power loads with production and 20% of the "Winter" load. -The arguments of the method `print_catalogue` can be used to filter the output. If you want to print the LV networks +The arguments of the method `get_catalogue` can be used to filter the output. If you want to get the LV networks only, you can call: ```pycon ->>> ElectricalNetwork.print_catalogue(name="LVFeeder") +>>> ElectricalNetwork.get_catalogue(name="LVFeeder") ``` | Name | Nb buses | Nb branches | Nb loads | Nb sources | Nb grounds | Nb potential refs | Available load points | -| :------------ | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | --------------------: | -| LVFeeder00939 | 8 | 7 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder02639 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder04790 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06713 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06926 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06975 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder18498 | 18 | 17 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder18769 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder19558 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder20256 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder23832 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder24400 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder27429 | 11 | 10 | 18 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder27681 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder30216 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder31441 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder36284 | 5 | 4 | 6 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder36360 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder37263 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| :------------ | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | :-------------------- | +| LVFeeder00939 | 8 | 7 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder02639 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder04790 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06713 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06926 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06975 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder18498 | 18 | 17 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder18769 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder19558 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder20256 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder23832 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder24400 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder27429 | 11 | 10 | 18 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder27681 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder30216 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder31441 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder36284 | 5 | 4 | 6 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder36360 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder37263 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | A regular expression can also be used: ```pycon ->>> ElectricalNetwork.print_catalogue(name=r"LVFeeder38[0-9]+") +>>> ElectricalNetwork.get_catalogue(name=r"LVFeeder38[0-9]+") ``` | Name | Nb buses | Nb branches | Nb loads | Nb sources | Nb grounds | Nb potential refs | Available load points | -| :------------ | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | --------------------: | -| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| :------------ | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | :-------------------- | +| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | ### Getting an instance -To build a network from the catalogue, the class method `from_catalogue` can be used. The name of the network and -the name of the load point must be provided: +You can build an `ElectricalNetwork` instance from the catalogue using the class method +`from_catalogue`. The name of the network and the name of the load point must be provided: ```pycon >>> en = ElectricalNetwork.from_catalogue(name="LVFeeder38211", load_point_name="Summer") ``` -In case of mistakes, an error is raised: +In case no or several results match the parameters, an error is raised: ```pycon >>> ElectricalNetwork.from_catalogue(name="LVFeeder38211", load_point_name="Unknown") -RoseauLoadFlowException: No load point matching the name 'Unknown' has been found for the network 'LVFeeder38211'. -Please look at the catalogue using the `print_catalogue` class method. [catalogue_not_found] +RoseauLoadFlowException: No load points for network 'LVFeeder38211' matching the query (load_point_name='Unknown') +have been found. Please look at the catalogue using the `get_catalogue` class method. [catalogue_not_found] ``` (catalogues-transformers)= @@ -159,13 +156,13 @@ The available transformers data come from the following data sheets: Pull requests to add some other sources are welcome! -### Printing the catalogue +### Inspecting the catalogue -This catalogue can be printed to the terminal: +This catalogue can be retrieved in the form of a dataframe using: ```pycon >>> from roseau.load_flow import TransformerParameters ->>> TransformerParameters.print_catalogue() +>>> TransformerParameters.get_catalogue() ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | @@ -313,11 +310,11 @@ The following data are available in this table: - the primary side phase to phase voltage, noted **uhv**. - the secondary side phase to phase volage, noted **ulv**. -The `print_catalogue` method accepts arguments (in bold above) that can be used to filter the printed table. The -following command only prints transformer parameters of transformers with an efficiency of "A0Ak": +The `get_catalogue` method accepts arguments (in bold above) that can be used to filter the returned table. The +following command only retrieves transformer parameters of transformers with an efficiency of "A0Ak": ```pycon ->>> TransformerParameters.print_catalogue(efficiency="A0Ak") +>>> TransformerParameters.get_catalogue(efficiency="A0Ak") ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | @@ -340,7 +337,7 @@ following command only prints transformer parameters of transformers with an eff or only transformers with a wye winding on the primary side (using a regular expression) ```pycon ->>> TransformerParameters.print_catalogue(type=r"^y.*$") +>>> TransformerParameters.get_catalogue(type=r"^y.*$") ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | @@ -353,22 +350,23 @@ or only transformers with a wye winding on the primary side (using a regular exp or only transformers meeting both criteria ```pycon ->>> TransformerParameters.print_catalogue(efficiency="A0Ak", type=r"^y.*$") +>>> TransformerParameters.get_catalogue(efficiency="A0Ak", type=r"^y.*$") ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | | :------------------- | :----------- | :------------ | :--------- | :---- | ------------------: | ----------------: | ---------------: | | SE_Minera_A0Ak_50kVA | SE | Minera | A0Ak | Yzn11 | 50.0 | 20.0 | 0.4 | -Among all the possible filters, the nominal power and voltages are expected in their default unit (VA and V). The -[Pint](https://pint.readthedocs.io/en/stable/) library can also be used. For instance, if we want to print -transformer parameters with a nominal power of 3150 kVA, the following two commands print the same table: +Among all the possible filters, the nominal power and voltages are expected in their default unit +(VA and V). You can also use the [Pint](https://pint.readthedocs.io/en/stable/) library to express +the values in different units. For instance, if you want to get transformer parameters with a +nominal power of 3150 kVA, the following two commands return the same table: ```pycon ->>> TransformerParameters.print_catalogue(sn=3150e3) # in VA by default +>>> TransformerParameters.get_catalogue(sn=3150e3) # in VA by default ->>> from roseau.load_flow.units import Q_ -... TransformerParameters.print_catalogue(sn=Q_(3150, "kVA")) +>>> from roseau.load_flow import Q_ +... TransformerParameters.get_catalogue(sn=Q_(3150, "kVA")) ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | @@ -379,11 +377,11 @@ transformer parameters with a nominal power of 3150 kVA, the following two comma ### Getting an instance -To build a transformer parameters from the catalogue, the class method `from_catalogue` can be used. The same filter -as the one used for the method `print_catalogue` can be used. The filter must lead to a single transformer in the -catalogue. +You can build a `TransformerParameters` instance from the catalogue using the class method `from_catalogue`. +You must filter the data to get a single transformer. You can apply the same filtering technique used for +the method `get_catalogue` to narrow down the result to a single transformer in the catalogue. -For instance, this filter leads to a single transformer parameters in the catalogue: +For instance, these parameters filter the catalogue down to a single transformer parameters: ```pycon >>> TransformerParameters.from_catalogue(efficiency="A0Ak", type=r"^y.*$") @@ -397,19 +395,188 @@ The `id` filter can be directly used: TransformerParameters(id='SE_Minera_A0Ak_50kVA') ``` -In case of mistakes, an error is raised: +In case no or several results match the parameters, an error is raised: ```pycon >>> TransformerParameters.from_catalogue(manufacturer="ft") -RoseauLoadFlowException: Several transformers matching the query ("manufacturer='ft'") -have been found. Please look at the catalogue using the `print_catalogue` class method. - [catalogue_several_found] +RoseauLoadFlowException: Several transformers matching the query (manufacturer='ft') have been found: +'FT_Standard_Standard_100kVA', 'FT_Standard_Standard_160kVA', 'FT_Standard_Standard_250kVA', +'FT_Standard_Standard_315kVA', 'FT_Standard_Standard_400kVA', 'FT_Standard_Standard_500kVA', +'FT_Standard_Standard_630kVA', 'FT_Standard_Standard_800kVA', 'FT_Standard_Standard_1000kVA', +'FT_Standard_Standard_1250kVA', 'FT_Standard_Standard_1600kVA', 'FT_Standard_Standard_2000kVA', +'FT_Standard_Standard_2500kVA', 'FT_Standard_Standard_3150kVA'. [catalogue_several_found] ``` or if no results: ```pycon >>> TransformerParameters.from_catalogue(manufacturer="unknown") -RoseauLoadFlowException: No manufacturer matching the name 'unknown' has been found. -Available manufacturers are 'FT', 'SE'. [catalogue_not_found] +RoseauLoadFlowException: No manufacturer matching 'unknown' has been found. Available manufacturers +are 'FT', 'SE'. [catalogue_not_found] +``` + +(catalogues-lines)= + +## Lines + +_Roseau Load Flow_ is provided with a catalogue of line parameters. These parameters are available +through the class `LineParameters`. + +### Source of data + +The available lines data are based on the following sources: + +- IEC standards including: IEC-60228, IEC-60287, IEC-60364 +- Technique de l'ingénieur (French technical and scientific documentation) + +### Inspecting the catalogue + +This catalogue can be retrieved in the form of a dataframe using: + +```pycon +>>> from roseau.load_flow import LineParameters +>>> LineParameters.get_catalogue() +``` + +_Truncated output_ + +| Name | Line type | Conductor material | Insulator type | Cross-section (mm²) | Resistance (ohm/km) | Reactance (ohm/km) | Susceptance (µS/km) | Maximal current (A) | +| :------- | :---------- | :----------------- | :------------- | ------------------: | ------------------: | -----------------: | ------------------: | ------------------: | +| T_AM_80 | twisted | am | | 80 | 0.457596 | 0.105575 | 3.0507e-05 | 203 | +| U_CU_19 | underground | cu | | 19 | 1.009 | 0.133054 | 2.33629e-05 | 138 | +| O_AM_33 | overhead | am | | 33 | 1.08577 | 0.375852 | 3.045e-06 | 142 | +| U_CU_150 | underground | cu | | 150 | 0.124 | 0.0960503 | 3.41234e-05 | 420 | +| O_AM_74 | overhead | am | | 74 | 0.491898 | 0.350482 | 3.2757e-06 | 232 | +| T_AM_34 | twisted | am | | 34 | 1.04719 | 0.121009 | 2.60354e-05 | 118 | +| T_AM_50 | twisted | am | | 50 | 0.744842 | 0.113705 | 2.79758e-05 | 146 | +| O_AM_95 | overhead | am | | 95 | 0.37184 | 0.342634 | 3.3543e-06 | 266 | +| U_CU_100 | underground | cu | | 100 | 0.185 | 0.102016 | 3.17647e-05 | 339 | +| T_CU_38 | twisted | cu | | 38 | 0.4966 | 0.118845 | 2.65816e-05 | 165 | +| O_AM_100 | overhead | am | | 100 | 0.356269 | 0.341022 | 3.371e-06 | 276 | +| U_AM_60 | underground | am | | 60 | 0.629804 | 0.11045 | 2.89372e-05 | 194 | +| T_AM_79 | twisted | am | | 79 | 0.463313 | 0.105781 | 3.04371e-05 | 201 | +| T_CU_60 | twisted | cu | | 60 | 0.3275 | 0.11045 | 2.89372e-05 | 219 | +| U_AM_240 | underground | am | | 240 | 0.14525 | 0.0899296 | 3.69374e-05 | 428 | +| O_AL_37 | overhead | al | | 37 | 0.837733 | 0.372257 | 3.0757e-06 | 152 | +| U_AM_93 | underground | am | | 93 | 0.383274 | 0.103152 | 3.13521e-05 | 249 | +| O_AM_28 | overhead | am | | 28 | 1.27866 | 0.381013 | 3.0019e-06 | 130 | +| T_AL_90 | twisted | al | | 90 | 0.3446 | 0.103672 | 3.11668e-05 | 219 | +| O_AM_79 | overhead | am | | 79 | 0.463313 | 0.348428 | 3.2959e-06 | 240 | + +The following data are available in this table: + +- the **name**. A name that contains the type of the line, the material of the conductor, the + cross-section area, and optionally the insulator type. It is in the form + `{line_type}_{conductor_material}_{cross_section}_{insulator_type}`. +- the **line type**. It can be `"OVERHEAD"`, `"UNDERGROUND"` or `"TWISTED"`. +- the **conductor material**. See the {class}`~roseau.load_flow.ConductorType` class. +- the **insulator type**. See the {class}`~roseau.load_flow.InsulatorType` class. +- the **cross-section** of the conductor in mm². + +in addition to the following calculated physical parameters: + +- the _resistance_ of the line in ohm/km. +- the _reactance_ of the line in ohm/km. +- the _susceptance_ of the line in µS/km. +- the _maximal current_ of the line in A. + +The `get_catalogue` method accepts arguments (in bold above) that can be used to filter the returned +table. The following command only returns line parameters made of Aluminum: + +```pycon +>>> LineParameters.get_catalogue(conductor_type="al") +``` + +_Truncated output_ + +| Name | Line type | Conductor material | Insulator type | Cross-section (mm²) | Resistance (ohm/km) | Reactance (ohm/km) | Susceptance (µS/km) | Maximal current (A) | +| :------- | :---------- | :----------------- | :------------- | ------------------: | ------------------: | -----------------: | ------------------: | ------------------: | +| U_AL_117 | underground | al | | 117 | 0.26104 | 0.0996298 | 3.2668e-05 | 286 | +| U_AL_33 | underground | al | | 33 | 0.9344 | 0.121598 | 2.58907e-05 | 144 | +| U_AL_69 | underground | al | | 69 | 0.4529 | 0.108041 | 2.96921e-05 | 212 | +| T_AL_228 | twisted | al | | 228 | 0.133509 | 0.0905569 | 3.66279e-05 | 395 | +| U_AL_150 | underground | al | | 150 | 0.206 | 0.0960503 | 3.41234e-05 | 325 | +| T_AL_69 | twisted | al | | 69 | 0.4529 | 0.108041 | 2.96921e-05 | 185 | +| O_AL_116 | overhead | al | | 116 | 0.26372 | 0.336359 | 3.42e-06 | 310 | +| U_AL_50 | underground | al | | 50 | 0.641 | 0.113705 | 2.79758e-05 | 175 | +| U_AL_93 | underground | al | | 93 | 0.32984 | 0.103152 | 3.13521e-05 | 249 | +| T_AL_59 | twisted | al | | 59 | 0.5519 | 0.110744 | 2.88474e-05 | 164 | + +or only lines with a cross section of 240 mm² (using a regular expression) + +```pycon +>>> LineParameters.get_catalogue(section=240) +``` + +| Name | Line type | Conductor material | Insulator type | Cross-section (mm²) | Resistance (ohm/km) | Reactance (ohm/km) | Susceptance (µS/km) | Maximal current (A) | +| :------- | :---------- | :----------------- | :------------- | ------------------: | ------------------: | -----------------: | ------------------: | ------------------: | +| O_AL_240 | overhead | al | | 240 | 0.125 | 0.313518 | 3.6823e-06 | 490 | +| O_CU_240 | overhead | cu | | 240 | 0.0775 | 0.313518 | 3.6823e-06 | 630 | +| O_AM_240 | overhead | am | | 240 | 0.14525 | 0.313518 | 3.6823e-06 | 490 | +| U_AL_240 | underground | al | | 240 | 0.125 | 0.0899296 | 3.69374e-05 | 428 | +| U_CU_240 | underground | cu | | 240 | 0.0775 | 0.0899296 | 3.69374e-05 | 549 | +| U_AM_240 | underground | am | | 240 | 0.14525 | 0.0899296 | 3.69374e-05 | 428 | +| T_AL_240 | twisted | al | | 240 | 0.125 | 0.0899296 | 3.69374e-05 | 409 | +| T_CU_240 | twisted | cu | | 240 | 0.0775 | 0.0899296 | 3.69374e-05 | 538 | +| T_AM_240 | twisted | am | | 240 | 0.14525 | 0.0899296 | 3.69374e-05 | 409 | + +or only lines meeting both criteria + +```pycon +>>> LineParameters.get_catalogue(conductor_type="al", section=240) +``` + +| Name | Line type | Conductor material | Insulator type | Cross-section (mm²) | Resistance (ohm/km) | Reactance (ohm/km) | Susceptance (µS/km) | Maximal current (A) | +| :------- | :---------- | :----------------- | :------------- | ------------------: | ------------------: | -----------------: | ------------------: | ------------------: | +| O_AL_240 | overhead | al | | 240 | 0.125 | 0.313518 | 3.6823e-06 | 490 | +| U_AL_240 | underground | al | | 240 | 0.125 | 0.0899296 | 3.69374e-05 | 428 | +| T_AL_240 | twisted | al | | 240 | 0.125 | 0.0899296 | 3.69374e-05 | 409 | + +When filtering by the cross-section area, it is expected to provide a numeric value in mm² or to use a pint quantity. + +### Getting an instance + +You can build a `LineParameters` instance from the catalogue using the class method `from_catalogue`. +You must filter the data to get a single line. You can apply the same filtering technique used for +the method `get_catalogue` to narrow down the result to a single line in the catalogue. + +For instance, these parameters filter the results down to a single line parameters: + +```pycon +>>> LineParameters.from_catalogue(line_type="underground", conductor_type="al", section=240) +LineParameters(id='U_AL_240') +``` + +Or you can use the `name` filter directly: + +```pycon +>>> LineParameters.from_catalogue(name="U_AL_240") +LineParameters(id='U_AL_240') +``` + +As you can see, the `id` of the created instance is the same as the name in the catalogue. You can +override this behaviour by passing the `id` parameter to `from_catalogue`. + +In case no or several results match the parameters, an error is raised: + +```pycon +>>> LineParameters.from_catalogue(name= r"^U_AL") +RoseauLoadFlowException: Several line parameters matching the query (name='^U_AL_') have been found: +'U_AL_19', 'U_AL_20', 'U_AL_22', 'U_AL_25', 'U_AL_28', 'U_AL_29', 'U_AL_33', 'U_AL_34', 'U_AL_37', +'U_AL_38', 'U_AL_40', 'U_AL_43', 'U_AL_48', 'U_AL_50', 'U_AL_54', 'U_AL_55', 'U_AL_59', 'U_AL_60', +'U_AL_69', 'U_AL_70', 'U_AL_74', 'U_AL_75', 'U_AL_79', 'U_AL_80', 'U_AL_90', 'U_AL_93', 'U_AL_95', +'U_AL_100', 'U_AL_116', 'U_AL_117', 'U_AL_120', 'U_AL_147', 'U_AL_148', 'U_AL_150', 'U_AL_228', +'U_AL_240', 'U_AL_288'. [catalogue_several_found] +``` + +or if no results: + +```pycon +>>> LineParameters.from_catalogue(name="unknown") +RoseauLoadFlowException: No name matching 'unknown' has been found. Available names are 'O_AL_12', +'O_AL_13', 'O_AL_14', 'O_AL_19', 'O_AL_20', 'O_AL_22', 'O_AL_25', 'O_AL_28', 'O_AL_29', 'O_AL_33', +'O_AL_34', 'O_AL_37', 'O_AL_38', 'O_AL_40', 'O_AL_43', 'O_AL_48', 'O_AL_50', 'O_AL_54', 'O_AL_55', +'O_AL_59', 'O_AL_60', 'O_AL_69', 'O_AL_70', 'O_AL_74', 'O_AL_75', 'O_AL_79', 'O_AL_80', 'O_AL_90', +'O_AL_93', 'O_AL_95', 'O_AL_100', 'O_AL_116', 'O_AL_117', 'O_AL_120', 'O_AL_147', 'O_AL_148', 'O_AL_150', +'O_AL_228', 'O_AL_240', 'O_AL_288', 'O_CU_3', 'O_CU_7', 'O_CU_12', 'O_CU_13', [...]. [catalogue_not_found] ``` diff --git a/poetry.lock b/poetry.lock index 5b656fed..dde1d9ae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -868,16 +868,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1528,7 +1518,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1536,15 +1525,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1561,7 +1543,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1569,7 +1550,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1698,24 +1678,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "rich" -version = "13.7.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - [[package]] name = "ruff" version = "0.1.11" @@ -2184,4 +2146,4 @@ plot = ["matplotlib"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c467c96774a04a058c3b480f1a8d5c6a250bc6f82b8fe6f13dde61e2e7d765f3" +content-hash = "3d6286235226a2f20c7bf0dd6e466fb22f42972cc2deb79ea2b5cb8682862f18" diff --git a/pyproject.toml b/pyproject.toml index efdb04cf..9bcdb7d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ shapely = ">=2.0.0" regex = ">=2022.1.18" pint = ">=0.21.0" typing-extensions = ">=4.6.2" -rich = ">=13.5.1" pyproj = ">=3.3.0" certifi = ">=2023.5.7" platformdirs = ">=4.0.0" diff --git a/roseau/load_flow/_compat.py b/roseau/load_flow/_compat.py new file mode 100644 index 00000000..634169f3 --- /dev/null +++ b/roseau/load_flow/_compat.py @@ -0,0 +1,41 @@ +import sys +from enum import Enum + +from typing_extensions import Self + +if sys.version_info >= (3, 11): + from enum import StrEnum as StrEnum +else: + + class StrEnum(str, Enum): + """ + Enum where members are also (and must be) strings. This is a backport of + `enum.StrEnum` from Python 3.11. + """ + + def __new__(cls, *values) -> Self: + "values must already be of type `str`" + if len(values) > 3: + raise TypeError(f"too many arguments for str(): {values!r}") + if len(values) == 1 and not isinstance(values[0], str): + # it must be a string + raise TypeError(f"{values[0]!r} is not a string") + if len(values) >= 2 and not isinstance(values[1], str): + # check that encoding argument is a string + raise TypeError(f"encoding must be a string, not {values[1]!r}") + if len(values) == 3 and not isinstance(values[2], str): + # check that errors argument is a string + raise TypeError(f"errors must be a string, not {values[2]!r}") + value = str(*values) + member = str.__new__(cls, value) + member._value_ = value + return member + + def __str__(self) -> str: + return str.__str__(self) + + def _generate_next_value_(name, start, count, last_values) -> str: # noqa: N805 + """ + Return the lower-cased version of the member name. + """ + return name.lower() diff --git a/roseau/load_flow/conftest.py b/roseau/load_flow/conftest.py index e2181a46..d8079b39 100644 --- a/roseau/load_flow/conftest.py +++ b/roseau/load_flow/conftest.py @@ -4,8 +4,6 @@ import pytest from pandas.testing import assert_frame_equal -from roseau.load_flow.utils import console - # Variable to test the network HERE = Path(__file__).parent.expanduser().absolute() TEST_ALL_NETWORKS_DATA_FOLDER = HERE / "tests" / "data" / "networks" @@ -80,11 +78,6 @@ def dgs_network_path(request) -> Path: return request.param -@pytest.fixture(autouse=True, scope="session") -def _set_console_width() -> None: - console.width = 210 - - # # Utils # diff --git a/roseau/load_flow/data/lines/Catalogue.csv b/roseau/load_flow/data/lines/Catalogue.csv new file mode 100644 index 00000000..76647d43 --- /dev/null +++ b/roseau/load_flow/data/lines/Catalogue.csv @@ -0,0 +1,356 @@ +name,type,material,insulator,section,r,x,b,maximal_current +O_AL_12,overhead,al,,12,2.69,0.4076321335,2.798e-06,70 +O_AL_13,overhead,al,,13,2.495,0.4051175176,2.8161e-06,76 +O_AL_14,overhead,al,,14,2.3,0.402789347,2.8331e-06,82 +O_AL_19,overhead,al,,19,1.6733333333,0.3931954996,2.9051e-06,103 +O_AL_20,overhead,al,,20,1.5944444444,0.3915840732,2.9175e-06,106 +O_AL_22,overhead,al,,22,1.4366666667,0.3885898156,2.9409e-06,113 +O_AL_25,overhead,al,,25,1.2,0.3845738118,2.973e-06,122 +O_AL_28,overhead,al,,28,1.1004,0.3810134861,3.0019e-06,130 +O_AL_29,overhead,al,,29,1.0672,0.3799110598,3.011e-06,132 +O_AL_33,overhead,al,,33,0.9344,0.3758517535,3.045e-06,142 +O_AL_34,overhead,al,,34,0.9012,0.374913895,3.0529e-06,144 +O_AL_37,overhead,al,,37,0.8377333333,0.3722574463,3.0757e-06,152 +O_AL_38,overhead,al,,38,0.8226,0.3714196387,3.0829e-06,155 +O_AL_40,overhead,al,,40,0.7923333333,0.3698082123,3.0969e-06,160 +O_AL_43,overhead,al,,43,0.7469333333,0.3675361917,3.1169e-06,167 +O_AL_48,overhead,al,,48,0.6712666667,0.3640804116,3.1478e-06,180 +O_AL_50,overhead,al,,50,0.641,0.3627979509,3.1595e-06,185 +O_AL_54,overhead,al,,54,0.6014,0.3603801484,3.1816e-06,193 +O_AL_55,overhead,al,,55,0.5915,0.3598036933,3.187e-06,195 +O_AL_59,overhead,al,,59,0.5519,0.3575981614,3.2075e-06,203 +O_AL_60,overhead,al,,60,0.542,0.3570701502,3.2125e-06,206 +O_AL_69,overhead,al,,69,0.4529,0.3526793993,3.2543e-06,224 +O_AL_70,overhead,al,,70,0.443,0.3522273638,3.2587e-06,226 +O_AL_74,overhead,al,,74,0.42332,0.3504815854,3.2757e-06,232 +O_AL_75,overhead,al,,75,0.4184,0.3500598888,3.2798e-06,234 +O_AL_79,overhead,al,,79,0.39872,0.3484275255,3.2959e-06,240 +O_AL_80,overhead,al,,80,0.3938,0.3480323514,3.2999e-06,242 +O_AL_90,overhead,al,,90,0.3446,0.3443320882,3.337e-06,258 +O_AL_93,overhead,al,,93,0.32984,0.3433019655,3.3475e-06,263 +O_AL_95,overhead,al,,95,0.32,0.3426335163,3.3543e-06,266 +O_AL_100,overhead,al,,100,0.3066,0.3410220899,3.371e-06,276 +O_AL_116,overhead,al,,116,0.26372,0.336359338,3.42e-06,310 +O_AL_117,overhead,al,,117,0.26104,0.3360896717,3.4229e-06,312 +O_AL_120,overhead,al,,120,0.253,0.3352942893,3.4314e-06,318 +O_AL_147,overhead,al,,147,0.2107,0.3289187147,3.5012e-06,356 +O_AL_148,overhead,al,,148,0.2091333333,0.3287057245,3.5036e-06,357 +O_AL_150,overhead,al,,150,0.206,0.3282840279,3.5083e-06,360 +O_AL_228,overhead,al,,228,0.1335090909,0.3151298548,3.6625e-06,474 +O_AL_240,overhead,al,,240,0.125,0.3135184284,3.6823e-06,490 +O_AL_288,overhead,al,,288,0.105,0.3077906278,3.7545e-06,552 +O_CU_3,overhead,cu,,3,6.4766666667,0.4511838553,2.5182e-06,35 +O_CU_7,overhead,cu,,7,2.7675,0.424565208,2.6822e-06,59 +O_CU_12,overhead,cu,,12,1.6033333333,0.4076321335,2.798e-06,90 +O_CU_13,overhead,cu,,13,1.49,0.4051175176,2.8161e-06,98 +O_CU_14,overhead,cu,,14,1.3766666667,0.402789347,2.8331e-06,105 +O_CU_19,overhead,cu,,19,1.009,0.3931954996,2.9051e-06,132 +O_CU_20,overhead,cu,,20,0.962,0.3915840732,2.9175e-06,136 +O_CU_22,overhead,cu,,22,0.868,0.3885898156,2.9409e-06,145 +O_CU_25,overhead,cu,,25,0.727,0.3845738118,2.973e-06,157 +O_CU_28,overhead,cu,,28,0.6661,0.3810134861,3.0019e-06,167 +O_CU_29,overhead,cu,,29,0.6458,0.3799110598,3.011e-06,170 +O_CU_33,overhead,cu,,33,0.5646,0.3758517535,3.045e-06,183 +O_CU_34,overhead,cu,,34,0.5443,0.374913895,3.0529e-06,187 +O_CU_37,overhead,cu,,37,0.5057333333,0.3722574463,3.0757e-06,196 +O_CU_38,overhead,cu,,38,0.4966,0.3714196387,3.0829e-06,199 +O_CU_40,overhead,cu,,40,0.4783333333,0.3698082123,3.0969e-06,204 +O_CU_43,overhead,cu,,43,0.4509333333,0.3675361917,3.1169e-06,213 +O_CU_48,overhead,cu,,48,0.4052666667,0.3640804116,3.1478e-06,227 +O_CU_50,overhead,cu,,50,0.387,0.3627979509,3.1595e-06,233 +O_CU_54,overhead,cu,,54,0.3632,0.3603801484,3.1816e-06,245 +O_CU_55,overhead,cu,,55,0.35725,0.3598036933,3.187e-06,248 +O_CU_59,overhead,cu,,59,0.33345,0.3575981614,3.2075e-06,260 +O_CU_60,overhead,cu,,60,0.3275,0.3570701502,3.2125e-06,262 +O_CU_69,overhead,cu,,69,0.27395,0.3526793993,3.2543e-06,289 +O_CU_70,overhead,cu,,70,0.268,0.3522273638,3.2587e-06,292 +O_CU_74,overhead,cu,,74,0.256,0.3504815854,3.2757e-06,302 +O_CU_75,overhead,cu,,75,0.253,0.3500598888,3.2798e-06,305 +O_CU_79,overhead,cu,,79,0.241,0.3484275255,3.2959e-06,315 +O_CU_80,overhead,cu,,80,0.238,0.3480323514,3.2999e-06,318 +O_CU_90,overhead,cu,,90,0.208,0.3443320882,3.337e-06,343 +O_CU_93,overhead,cu,,93,0.199,0.3433019655,3.3475e-06,351 +O_CU_95,overhead,cu,,95,0.193,0.3426335163,3.3543e-06,356 +O_CU_100,overhead,cu,,100,0.185,0.3410220899,3.371e-06,367 +O_CU_116,overhead,cu,,116,0.1594,0.336359338,3.42e-06,401 +O_CU_117,overhead,cu,,117,0.1578,0.3360896717,3.4229e-06,403 +O_CU_120,overhead,cu,,120,0.153,0.3352942893,3.4314e-06,409 +O_CU_147,overhead,cu,,147,0.1269,0.3289187147,3.5012e-06,459 +O_CU_148,overhead,cu,,148,0.1259333333,0.3287057245,3.5036e-06,461 +O_CU_150,overhead,cu,,150,0.124,0.3282840279,3.5083e-06,465 +O_CU_228,overhead,cu,,228,0.0826272727,0.3151298548,3.6625e-06,609 +O_CU_240,overhead,cu,,240,0.0775,0.3135184284,3.6823e-06,630 +O_CU_288,overhead,cu,,288,0.0651,0.3077906278,3.7545e-06,705 +O_AM_12,overhead,am,,12,3.12578,0.4076321335,2.798e-06,70 +O_AM_13,overhead,am,,13,2.89919,0.4051175176,2.8161e-06,76 +O_AM_14,overhead,am,,14,2.6726,0.402789347,2.8331e-06,82 +O_AM_19,overhead,am,,19,1.9444133333,0.3931954996,2.9051e-06,103 +O_AM_20,overhead,am,,20,1.8527444444,0.3915840732,2.9175e-06,106 +O_AM_22,overhead,am,,22,1.6694066667,0.3885898156,2.9409e-06,113 +O_AM_25,overhead,am,,25,1.3944,0.3845738118,2.973e-06,122 +O_AM_28,overhead,am,,28,1.2786648,0.3810134861,3.0019e-06,130 +O_AM_29,overhead,am,,29,1.2400864,0.3799110598,3.011e-06,132 +O_AM_33,overhead,am,,33,1.0857728,0.3758517535,3.045e-06,142 +O_AM_34,overhead,am,,34,1.0471944,0.374913895,3.0529e-06,144 +O_AM_37,overhead,am,,37,0.9734461333,0.3722574463,3.0757e-06,152 +O_AM_38,overhead,am,,38,0.9558612,0.3714196387,3.0829e-06,155 +O_AM_40,overhead,am,,40,0.9206913333,0.3698082123,3.0969e-06,160 +O_AM_43,overhead,am,,43,0.8679365333,0.3675361917,3.1169e-06,167 +O_AM_48,overhead,am,,48,0.7800118667,0.3640804116,3.1478e-06,180 +O_AM_50,overhead,am,,50,0.744842,0.3627979509,3.1595e-06,185 +O_AM_54,overhead,am,,54,0.6988268,0.3603801484,3.1816e-06,193 +O_AM_55,overhead,am,,55,0.687323,0.3598036933,3.187e-06,195 +O_AM_59,overhead,am,,59,0.6413078,0.3575981614,3.2075e-06,203 +O_AM_60,overhead,am,,60,0.629804,0.3570701502,3.2125e-06,206 +O_AM_69,overhead,am,,69,0.5262698,0.3526793993,3.2543e-06,224 +O_AM_70,overhead,am,,70,0.514766,0.3522273638,3.2587e-06,226 +O_AM_74,overhead,am,,74,0.49189784,0.3504815854,3.2757e-06,232 +O_AM_75,overhead,am,,75,0.4861808,0.3500598888,3.2798e-06,234 +O_AM_79,overhead,am,,79,0.46331264,0.3484275255,3.2959e-06,240 +O_AM_80,overhead,am,,80,0.4575956,0.3480323514,3.2999e-06,242 +O_AM_90,overhead,am,,90,0.4004252,0.3443320882,3.337e-06,258 +O_AM_93,overhead,am,,93,0.38327408,0.3433019655,3.3475e-06,263 +O_AM_95,overhead,am,,95,0.37184,0.3426335163,3.3543e-06,266 +O_AM_100,overhead,am,,100,0.3562692,0.3410220899,3.371e-06,276 +O_AM_116,overhead,am,,116,0.30644264,0.336359338,3.42e-06,310 +O_AM_117,overhead,am,,117,0.30332848,0.3360896717,3.4229e-06,312 +O_AM_120,overhead,am,,120,0.293986,0.3352942893,3.4314e-06,318 +O_AM_147,overhead,am,,147,0.2448334,0.3289187147,3.5012e-06,356 +O_AM_148,overhead,am,,148,0.2430129333,0.3287057245,3.5036e-06,357 +O_AM_150,overhead,am,,150,0.239372,0.3282840279,3.5083e-06,360 +O_AM_228,overhead,am,,228,0.1551375636,0.3151298548,3.6625e-06,474 +O_AM_240,overhead,am,,240,0.14525,0.3135184284,3.6823e-06,490 +O_AM_288,overhead,am,,288,0.12201,0.3077906278,3.7545e-06,552 +U_AL_19,underground,al,,19,1.6733333333,0.1330544178,2.33629e-05,107 +U_AL_20,underground,al,,20,1.5944444444,0.1319453158,2.35859e-05,110 +U_AL_22,underground,al,,22,1.4366666667,0.1299081697,2.40066e-05,116 +U_AL_25,underground,al,,25,1.2,0.1272251024,2.45842e-05,125 +U_AL_28,underground,al,,28,1.1004,0.124894529,2.51089e-05,132 +U_AL_29,underground,al,,29,1.0672,0.1241821772,2.52738e-05,135 +U_AL_33,underground,al,,33,0.9344,0.1215975746,2.58907e-05,144 +U_AL_34,underground,al,,34,0.9012,0.1210090932,2.60354e-05,147 +U_AL_37,underground,al,,37,0.8377333333,0.119360076,2.64496e-05,152 +U_AL_38,underground,al,,38,0.8226,0.1188454977,2.65816e-05,154 +U_AL_40,underground,al,,40,0.7923333333,0.1178632261,2.68372e-05,158 +U_AL_43,underground,al,,43,0.7469333333,0.116495047,2.72015e-05,163 +U_AL_48,underground,al,,48,0.6712666667,0.1144519385,2.77643e-05,172 +U_AL_50,underground,al,,50,0.641,0.113705448,2.79758e-05,175 +U_AL_54,underground,al,,54,0.6014,0.112315465,2.83783e-05,183 +U_AL_55,underground,al,,55,0.5915,0.1119874251,2.8475e-05,185 +U_AL_59,underground,al,,59,0.5519,0.1107443314,2.88474e-05,193 +U_AL_60,underground,al,,60,0.542,0.1104495588,2.89372e-05,194 +U_AL_69,underground,al,,69,0.4529,0.1080408311,2.96921e-05,212 +U_AL_70,underground,al,,70,0.443,0.1077971677,2.97707e-05,214 +U_AL_74,underground,al,,74,0.42332,0.1068637229,3.00755e-05,220 +U_AL_75,underground,al,,75,0.4184,0.1066400577,3.01495e-05,222 +U_AL_79,underground,al,,79,0.39872,0.1057809122,3.04371e-05,228 +U_AL_80,underground,al,,80,0.3938,0.1055745142,3.0507e-05,229 +U_AL_90,underground,al,,90,0.3446,0.1036719918,3.11668e-05,244 +U_AL_93,underground,al,,93,0.32984,0.1031520348,3.13521e-05,249 +U_AL_95,underground,al,,95,0.32,0.1028168921,3.14727e-05,252 +U_AL_100,underground,al,,100,0.3066,0.1020162744,3.17647e-05,260 +U_AL_116,underground,al,,116,0.26372,0.0997578116,3.26182e-05,285 +U_AL_117,underground,al,,117,0.26104,0.0996298374,3.2668e-05,286 +U_AL_120,underground,al,,120,0.253,0.0992540572,3.28149e-05,291 +U_AL_147,underground,al,,147,0.2107,0.0963323591,3.40041e-05,322 +U_AL_148,underground,al,,148,0.2091333333,0.0962375209,3.40441e-05,323 +U_AL_150,underground,al,,150,0.206,0.0960502777,3.41234e-05,325 +U_AL_228,underground,al,,228,0.1335090909,0.0905569282,3.66279e-05,415 +U_AL_240,underground,al,,240,0.125,0.0899296465,3.69374e-05,428 +U_AL_288,underground,al,,288,0.105,0.0877788677,3.80397e-05,474 +U_CU_19,underground,cu,,19,1.009,0.1330544178,2.33629e-05,138 +U_CU_20,underground,cu,,20,0.962,0.1319453158,2.35859e-05,142 +U_CU_22,underground,cu,,22,0.868,0.1299081697,2.40066e-05,149 +U_CU_25,underground,cu,,25,0.727,0.1272251024,2.45842e-05,161 +U_CU_28,underground,cu,,28,0.6661,0.124894529,2.51089e-05,170 +U_CU_29,underground,cu,,29,0.6458,0.1241821772,2.52738e-05,173 +U_CU_33,underground,cu,,33,0.5646,0.1215975746,2.58907e-05,186 +U_CU_34,underground,cu,,34,0.5443,0.1210090932,2.60354e-05,189 +U_CU_37,underground,cu,,37,0.5057333333,0.119360076,2.64496e-05,196 +U_CU_38,underground,cu,,38,0.4966,0.1188454977,2.65816e-05,199 +U_CU_40,underground,cu,,40,0.4783333333,0.1178632261,2.68372e-05,203 +U_CU_43,underground,cu,,43,0.4509333333,0.116495047,2.72015e-05,210 +U_CU_48,underground,cu,,48,0.4052666667,0.1144519385,2.77643e-05,221 +U_CU_50,underground,cu,,50,0.387,0.113705448,2.79758e-05,225 +U_CU_54,underground,cu,,54,0.3632,0.112315465,2.83783e-05,235 +U_CU_55,underground,cu,,55,0.35725,0.1119874251,2.8475e-05,238 +U_CU_59,underground,cu,,59,0.33345,0.1107443314,2.88474e-05,248 +U_CU_60,underground,cu,,60,0.3275,0.1104495588,2.89372e-05,250 +U_CU_69,underground,cu,,69,0.27395,0.1080408311,2.96921e-05,273 +U_CU_70,underground,cu,,70,0.268,0.1077971677,2.97707e-05,276 +U_CU_74,underground,cu,,74,0.256,0.1068637229,3.00755e-05,285 +U_CU_75,underground,cu,,75,0.253,0.1066400577,3.01495e-05,287 +U_CU_79,underground,cu,,79,0.241,0.1057809122,3.04371e-05,295 +U_CU_80,underground,cu,,80,0.238,0.1055745142,3.0507e-05,298 +U_CU_90,underground,cu,,90,0.208,0.1036719918,3.11668e-05,319 +U_CU_93,underground,cu,,93,0.199,0.1031520348,3.13521e-05,326 +U_CU_95,underground,cu,,95,0.193,0.1028168921,3.14727e-05,330 +U_CU_100,underground,cu,,100,0.185,0.1020162744,3.17647e-05,339 +U_CU_116,underground,cu,,116,0.1594,0.0997578116,3.26182e-05,368 +U_CU_117,underground,cu,,117,0.1578,0.0996298374,3.2668e-05,370 +U_CU_120,underground,cu,,120,0.153,0.0992540572,3.28149e-05,375 +U_CU_147,underground,cu,,147,0.1269,0.0963323591,3.40041e-05,416 +U_CU_148,underground,cu,,148,0.1259333333,0.0962375209,3.40441e-05,417 +U_CU_150,underground,cu,,150,0.124,0.0960502777,3.41234e-05,420 +U_CU_228,underground,cu,,228,0.0826272727,0.0905569282,3.66279e-05,533 +U_CU_240,underground,cu,,240,0.0775,0.0899296465,3.69374e-05,549 +U_CU_288,underground,cu,,288,0.0651,0.0877788677,3.80397e-05,605 +U_AM_19,underground,am,,19,1.9444133333,0.1330544178,2.33629e-05,107 +U_AM_20,underground,am,,20,1.8527444444,0.1319453158,2.35859e-05,110 +U_AM_22,underground,am,,22,1.6694066667,0.1299081697,2.40066e-05,116 +U_AM_25,underground,am,,25,1.3944,0.1272251024,2.45842e-05,125 +U_AM_28,underground,am,,28,1.2786648,0.124894529,2.51089e-05,132 +U_AM_29,underground,am,,29,1.2400864,0.1241821772,2.52738e-05,135 +U_AM_33,underground,am,,33,1.0857728,0.1215975746,2.58907e-05,144 +U_AM_34,underground,am,,34,1.0471944,0.1210090932,2.60354e-05,147 +U_AM_37,underground,am,,37,0.9734461333,0.119360076,2.64496e-05,152 +U_AM_38,underground,am,,38,0.9558612,0.1188454977,2.65816e-05,154 +U_AM_40,underground,am,,40,0.9206913333,0.1178632261,2.68372e-05,158 +U_AM_43,underground,am,,43,0.8679365333,0.116495047,2.72015e-05,163 +U_AM_48,underground,am,,48,0.7800118667,0.1144519385,2.77643e-05,172 +U_AM_50,underground,am,,50,0.744842,0.113705448,2.79758e-05,175 +U_AM_54,underground,am,,54,0.6988268,0.112315465,2.83783e-05,183 +U_AM_55,underground,am,,55,0.687323,0.1119874251,2.8475e-05,185 +U_AM_59,underground,am,,59,0.6413078,0.1107443314,2.88474e-05,193 +U_AM_60,underground,am,,60,0.629804,0.1104495588,2.89372e-05,194 +U_AM_69,underground,am,,69,0.5262698,0.1080408311,2.96921e-05,212 +U_AM_70,underground,am,,70,0.514766,0.1077971677,2.97707e-05,214 +U_AM_74,underground,am,,74,0.49189784,0.1068637229,3.00755e-05,220 +U_AM_75,underground,am,,75,0.4861808,0.1066400577,3.01495e-05,222 +U_AM_79,underground,am,,79,0.46331264,0.1057809122,3.04371e-05,228 +U_AM_80,underground,am,,80,0.4575956,0.1055745142,3.0507e-05,229 +U_AM_90,underground,am,,90,0.4004252,0.1036719918,3.11668e-05,244 +U_AM_93,underground,am,,93,0.38327408,0.1031520348,3.13521e-05,249 +U_AM_95,underground,am,,95,0.37184,0.1028168921,3.14727e-05,252 +U_AM_100,underground,am,,100,0.3562692,0.1020162744,3.17647e-05,260 +U_AM_116,underground,am,,116,0.30644264,0.0997578116,3.26182e-05,285 +U_AM_117,underground,am,,117,0.30332848,0.0996298374,3.2668e-05,286 +U_AM_120,underground,am,,120,0.293986,0.0992540572,3.28149e-05,291 +U_AM_147,underground,am,,147,0.2448334,0.0963323591,3.40041e-05,322 +U_AM_148,underground,am,,148,0.2430129333,0.0962375209,3.40441e-05,323 +U_AM_150,underground,am,,150,0.239372,0.0960502777,3.41234e-05,325 +U_AM_228,underground,am,,228,0.1551375636,0.0905569282,3.66279e-05,415 +U_AM_240,underground,am,,240,0.14525,0.0899296465,3.69374e-05,428 +U_AM_288,underground,am,,288,0.12201,0.0877788677,3.80397e-05,474 +T_AL_12,twisted,al,,12,2.69,0.1433737819,2.14745e-05,64 +T_AL_13,twisted,al,,13,2.495,0.1415282478,2.17895e-05,68 +T_AL_14,twisted,al,,14,2.3,0.1398372258,2.20863e-05,71 +T_AL_19,twisted,al,,19,1.6733333333,0.1330544178,2.33629e-05,84 +T_AL_20,twisted,al,,20,1.5944444444,0.1319453158,2.35859e-05,86 +T_AL_22,twisted,al,,22,1.4366666667,0.1299081697,2.40066e-05,90 +T_AL_25,twisted,al,,25,1.2,0.1272251024,2.45842e-05,97 +T_AL_28,twisted,al,,28,1.1004,0.124894529,2.51089e-05,104 +T_AL_29,twisted,al,,29,1.0672,0.1241821772,2.52738e-05,106 +T_AL_33,twisted,al,,33,0.9344,0.1215975746,2.58907e-05,115 +T_AL_34,twisted,al,,34,0.9012,0.1210090932,2.60354e-05,118 +T_AL_37,twisted,al,,37,0.8377333333,0.119360076,2.64496e-05,123 +T_AL_38,twisted,al,,38,0.8226,0.1188454977,2.65816e-05,125 +T_AL_40,twisted,al,,40,0.7923333333,0.1178632261,2.68372e-05,129 +T_AL_43,twisted,al,,43,0.7469333333,0.116495047,2.72015e-05,134 +T_AL_48,twisted,al,,48,0.6712666667,0.1144519385,2.77643e-05,143 +T_AL_50,twisted,al,,50,0.641,0.113705448,2.79758e-05,146 +T_AL_54,twisted,al,,54,0.6014,0.112315465,2.83783e-05,154 +T_AL_55,twisted,al,,55,0.5915,0.1119874251,2.8475e-05,156 +T_AL_59,twisted,al,,59,0.5519,0.1107443314,2.88474e-05,164 +T_AL_60,twisted,al,,60,0.542,0.1104495588,2.89372e-05,166 +T_AL_69,twisted,al,,69,0.4529,0.1080408311,2.96921e-05,185 +T_AL_70,twisted,al,,70,0.443,0.1077971677,2.97707e-05,187 +T_AL_74,twisted,al,,74,0.42332,0.1068637229,3.00755e-05,193 +T_AL_75,twisted,al,,75,0.4184,0.1066400577,3.01495e-05,195 +T_AL_79,twisted,al,,79,0.39872,0.1057809122,3.04371e-05,201 +T_AL_80,twisted,al,,80,0.3938,0.1055745142,3.0507e-05,203 +T_AL_90,twisted,al,,90,0.3446,0.1036719918,3.11668e-05,219 +T_AL_93,twisted,al,,93,0.32984,0.1031520348,3.13521e-05,224 +T_AL_95,twisted,al,,95,0.32,0.1028168921,3.14727e-05,227 +T_AL_100,twisted,al,,100,0.3066,0.1020162744,3.17647e-05,234 +T_AL_116,twisted,al,,116,0.26372,0.0997578116,3.26182e-05,257 +T_AL_117,twisted,al,,117,0.26104,0.0996298374,3.2668e-05,259 +T_AL_120,twisted,al,,120,0.253,0.0992540572,3.28149e-05,263 +T_AL_147,twisted,al,,147,0.2107,0.0963323591,3.40041e-05,300 +T_AL_148,twisted,al,,148,0.2091333333,0.0962375209,3.40441e-05,301 +T_AL_150,twisted,al,,150,0.206,0.0960502777,3.41234e-05,304 +T_AL_228,twisted,al,,228,0.1335090909,0.0905569282,3.66279e-05,395 +T_AL_240,twisted,al,,240,0.125,0.0899296465,3.69374e-05,409 +T_AL_288,twisted,al,,288,0.105,0.0877788677,3.80397e-05,459 +T_CU_3,twisted,cu,,3,6.4766666667,0.1780965781,1.68827e-05,35 +T_CU_7,twisted,cu,,7,2.7675,0.1562894945,1.95015e-05,59 +T_CU_12,twisted,cu,,12,1.6033333333,0.1433737819,2.14745e-05,83 +T_CU_13,twisted,cu,,13,1.49,0.1415282478,2.17895e-05,88 +T_CU_14,twisted,cu,,14,1.3766666667,0.1398372258,2.20863e-05,92 +T_CU_19,twisted,cu,,19,1.009,0.1330544178,2.33629e-05,109 +T_CU_20,twisted,cu,,20,0.962,0.1319453158,2.35859e-05,112 +T_CU_22,twisted,cu,,22,0.868,0.1299081697,2.40066e-05,118 +T_CU_25,twisted,cu,,25,0.727,0.1272251024,2.45842e-05,127 +T_CU_28,twisted,cu,,28,0.6661,0.124894529,2.51089e-05,136 +T_CU_29,twisted,cu,,29,0.6458,0.1241821772,2.52738e-05,139 +T_CU_33,twisted,cu,,33,0.5646,0.1215975746,2.58907e-05,152 +T_CU_34,twisted,cu,,34,0.5443,0.1210090932,2.60354e-05,155 +T_CU_37,twisted,cu,,37,0.5057333333,0.119360076,2.64496e-05,163 +T_CU_38,twisted,cu,,38,0.4966,0.1188454977,2.65816e-05,165 +T_CU_40,twisted,cu,,40,0.4783333333,0.1178632261,2.68372e-05,169 +T_CU_43,twisted,cu,,43,0.4509333333,0.116495047,2.72015e-05,176 +T_CU_48,twisted,cu,,48,0.4052666667,0.1144519385,2.77643e-05,187 +T_CU_50,twisted,cu,,50,0.387,0.113705448,2.79758e-05,192 +T_CU_54,twisted,cu,,54,0.3632,0.112315465,2.83783e-05,203 +T_CU_55,twisted,cu,,55,0.35725,0.1119874251,2.8475e-05,206 +T_CU_59,twisted,cu,,59,0.33345,0.1107443314,2.88474e-05,216 +T_CU_60,twisted,cu,,60,0.3275,0.1104495588,2.89372e-05,219 +T_CU_69,twisted,cu,,69,0.27395,0.1080408311,2.96921e-05,243 +T_CU_70,twisted,cu,,70,0.268,0.1077971677,2.97707e-05,246 +T_CU_74,twisted,cu,,74,0.256,0.1068637229,3.00755e-05,254 +T_CU_75,twisted,cu,,75,0.253,0.1066400577,3.01495e-05,256 +T_CU_79,twisted,cu,,79,0.241,0.1057809122,3.04371e-05,265 +T_CU_80,twisted,cu,,80,0.238,0.1055745142,3.0507e-05,267 +T_CU_90,twisted,cu,,90,0.208,0.1036719918,3.11668e-05,288 +T_CU_93,twisted,cu,,93,0.199,0.1031520348,3.13521e-05,294 +T_CU_95,twisted,cu,,95,0.193,0.1028168921,3.14727e-05,298 +T_CU_100,twisted,cu,,100,0.185,0.1020162744,3.17647e-05,308 +T_CU_116,twisted,cu,,116,0.1594,0.0997578116,3.26182e-05,338 +T_CU_117,twisted,cu,,117,0.1578,0.0996298374,3.2668e-05,340 +T_CU_120,twisted,cu,,120,0.153,0.0992540572,3.28149e-05,346 +T_CU_147,twisted,cu,,147,0.1269,0.0963323591,3.40041e-05,394 +T_CU_148,twisted,cu,,148,0.1259333333,0.0962375209,3.40441e-05,395 +T_CU_150,twisted,cu,,150,0.124,0.0960502777,3.41234e-05,399 +T_CU_228,twisted,cu,,228,0.0826272727,0.0905569282,3.66279e-05,520 +T_CU_240,twisted,cu,,240,0.0775,0.0899296465,3.69374e-05,538 +T_CU_288,twisted,cu,,288,0.0651,0.0877788677,3.80397e-05,604 +T_AM_12,twisted,am,,12,3.12578,0.1433737819,2.14745e-05,64 +T_AM_13,twisted,am,,13,2.89919,0.1415282478,2.17895e-05,68 +T_AM_14,twisted,am,,14,2.6726,0.1398372258,2.20863e-05,71 +T_AM_19,twisted,am,,19,1.9444133333,0.1330544178,2.33629e-05,84 +T_AM_20,twisted,am,,20,1.8527444444,0.1319453158,2.35859e-05,86 +T_AM_22,twisted,am,,22,1.6694066667,0.1299081697,2.40066e-05,90 +T_AM_25,twisted,am,,25,1.3944,0.1272251024,2.45842e-05,97 +T_AM_28,twisted,am,,28,1.2786648,0.124894529,2.51089e-05,104 +T_AM_29,twisted,am,,29,1.2400864,0.1241821772,2.52738e-05,106 +T_AM_33,twisted,am,,33,1.0857728,0.1215975746,2.58907e-05,115 +T_AM_34,twisted,am,,34,1.0471944,0.1210090932,2.60354e-05,118 +T_AM_37,twisted,am,,37,0.9734461333,0.119360076,2.64496e-05,123 +T_AM_38,twisted,am,,38,0.9558612,0.1188454977,2.65816e-05,125 +T_AM_40,twisted,am,,40,0.9206913333,0.1178632261,2.68372e-05,129 +T_AM_43,twisted,am,,43,0.8679365333,0.116495047,2.72015e-05,134 +T_AM_48,twisted,am,,48,0.7800118667,0.1144519385,2.77643e-05,143 +T_AM_50,twisted,am,,50,0.744842,0.113705448,2.79758e-05,146 +T_AM_54,twisted,am,,54,0.6988268,0.112315465,2.83783e-05,154 +T_AM_55,twisted,am,,55,0.687323,0.1119874251,2.8475e-05,156 +T_AM_59,twisted,am,,59,0.6413078,0.1107443314,2.88474e-05,164 +T_AM_60,twisted,am,,60,0.629804,0.1104495588,2.89372e-05,166 +T_AM_69,twisted,am,,69,0.5262698,0.1080408311,2.96921e-05,185 +T_AM_70,twisted,am,,70,0.514766,0.1077971677,2.97707e-05,187 +T_AM_74,twisted,am,,74,0.49189784,0.1068637229,3.00755e-05,193 +T_AM_75,twisted,am,,75,0.4861808,0.1066400577,3.01495e-05,195 +T_AM_79,twisted,am,,79,0.46331264,0.1057809122,3.04371e-05,201 +T_AM_80,twisted,am,,80,0.4575956,0.1055745142,3.0507e-05,203 +T_AM_90,twisted,am,,90,0.4004252,0.1036719918,3.11668e-05,219 +T_AM_93,twisted,am,,93,0.38327408,0.1031520348,3.13521e-05,224 +T_AM_95,twisted,am,,95,0.37184,0.1028168921,3.14727e-05,227 +T_AM_100,twisted,am,,100,0.3562692,0.1020162744,3.17647e-05,234 +T_AM_116,twisted,am,,116,0.30644264,0.0997578116,3.26182e-05,257 +T_AM_117,twisted,am,,117,0.30332848,0.0996298374,3.2668e-05,259 +T_AM_120,twisted,am,,120,0.293986,0.0992540572,3.28149e-05,263 +T_AM_147,twisted,am,,147,0.2448334,0.0963323591,3.40041e-05,300 +T_AM_148,twisted,am,,148,0.2430129333,0.0962375209,3.40441e-05,301 +T_AM_150,twisted,am,,150,0.239372,0.0960502777,3.41234e-05,304 +T_AM_228,twisted,am,,228,0.1551375636,0.0905569282,3.66279e-05,395 +T_AM_240,twisted,am,,240,0.14525,0.0899296465,3.69374e-05,409 +T_AM_288,twisted,am,,288,0.12201,0.0877788677,3.80397e-05,459 diff --git a/roseau/load_flow/exceptions.py b/roseau/load_flow/exceptions.py index 933b93de..41fe31c3 100644 --- a/roseau/load_flow/exceptions.py +++ b/roseau/load_flow/exceptions.py @@ -1,14 +1,12 @@ """ This module contains the exceptions used by Roseau Load Flow. """ -import unicodedata -from enum import Enum, auto -from typing import Union +from enum import auto -from typing_extensions import Self +from roseau.load_flow._compat import StrEnum -class RoseauLoadFlowExceptionCode(Enum): +class RoseauLoadFlowExceptionCode(StrEnum): """Error codes used by Roseau Load Flow.""" # Generic @@ -110,44 +108,18 @@ class RoseauLoadFlowExceptionCode(Enum): # License errors LICENSE_ERROR = auto() - @classmethod - def package_name(cls) -> str: - return "roseau.load_flow" - - def __str__(self) -> str: - return f"{self.package_name()}.{self.name}".lower() - def __eq__(self, other) -> bool: if isinstance(other, str): - return other.lower() == str(self).lower() + return other.lower() == self.lower() return super().__eq__(other) @classmethod - def from_string(cls, string: Union[str, "RoseauLoadFlowExceptionCode"]) -> Self: - """A method to convert a string into an error code enumerated type. - - Args: - string: - The string depicted the error code. If a good element is given - - Returns: - The enumerated type value corresponding with `string`. - """ - if isinstance(string, cls): - return string - elif isinstance(string, str): - pass - else: - string = str(string) - - # Withdraw accents and make lowercase - string = unicodedata.normalize("NFKD", string.lower()).encode("ASCII", "ignore").decode() - - # Withdraw the package prefix (e.g. roseau.core) - error_str = string.removeprefix(f"{cls.package_name()}.") - - # Get the value of this string - return cls[error_str.upper()] + def _missing_(cls, value: object) -> "RoseauLoadFlowExceptionCode | None": + if isinstance(value, str): + try: + return cls[value.upper().replace(" ", "_").replace("-", "_")] + except KeyError: + return None class RoseauLoadFlowException(Exception): diff --git a/roseau/load_flow/io/tests/test_dict.py b/roseau/load_flow/io/tests/test_dict.py index 2420586f..91331f8b 100644 --- a/roseau/load_flow/io/tests/test_dict.py +++ b/roseau/load_flow/io/tests/test_dict.py @@ -17,6 +17,7 @@ VoltageSource, ) from roseau.load_flow.network import ElectricalNetwork +from roseau.load_flow.utils import ConductorType, InsulatorType, LineType def test_to_dict(): @@ -30,7 +31,15 @@ def test_to_dict(): vs = VoltageSource("vs", source_bus, phases="abcn", voltages=voltages) # Same id, different line parameters -> fail - lp1 = LineParameters("test", z_line=np.eye(4, dtype=complex), y_shunt=np.eye(4, dtype=complex)) + lp1 = LineParameters( + "test", + z_line=np.eye(4, dtype=complex), + y_shunt=np.eye(4, dtype=complex), + line_type=LineType.UNDERGROUND, + conductor_type=ConductorType.AA, + insulator_type=InsulatorType.PVC, + section=120, + ) lp2 = LineParameters("test", z_line=np.eye(4, dtype=complex), y_shunt=np.eye(4, dtype=complex) * 1.1) geom = LineString([(0.0, 0.0), (0.0, 1.0)]) @@ -66,7 +75,12 @@ def test_to_dict(): assert "geometry" in res["branches"][1] assert np.isclose(res["buses"][0]["min_voltage"], 0.9 * vn) assert np.isclose(res["buses"][1]["max_voltage"], 1.1 * vn) - assert np.isclose(res["lines_params"][0]["max_current"], 1000) + lp_dict = res["lines_params"][0] + assert np.isclose(lp_dict["max_current"], 1000) + assert lp_dict["line_type"] == "UNDERGROUND" + assert lp_dict["conductor_type"] == "AA" + assert lp_dict["insulator_type"] == "PVC" + assert np.isclose(lp_dict["section"], 120) res = en.to_dict(_lf_only=True) assert "geometry" not in res["buses"][0] @@ -75,7 +89,12 @@ def test_to_dict(): assert "geometry" not in res["branches"][1] assert "min_voltage" not in res["buses"][0] assert "max_voltage" not in res["buses"][1] - assert "max_current" not in res["lines_params"][0] + lp_dict = res["lines_params"][0] + assert "max_current" not in lp_dict + assert "line_type" not in lp_dict + assert "conductor_type" not in lp_dict + assert "insulator_type" not in lp_dict + assert "section" not in lp_dict # Same id, different transformer parameters -> fail ground = Ground("ground") diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index f6762aa2..66d3335c 100644 --- a/roseau/load_flow/models/core.py +++ b/roseau/load_flow/models/core.py @@ -170,7 +170,7 @@ def _res_getter(self, value: _T | None, warning: bool) -> _T: return value @staticmethod - def _parse_geometry(geometry: str | None | Any) -> BaseGeometry | None: + def _parse_geometry(geometry: str | dict[str, Any] | None) -> BaseGeometry | None: if geometry is None: return None elif isinstance(geometry, str): diff --git a/roseau/load_flow/models/lines/lines.py b/roseau/load_flow/models/lines/lines.py index 1bbf3db6..a2de8314 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -172,11 +172,12 @@ def __init__( The second bus (aka `"to_bus"`) to connect to the line. parameters: - Parameters defining the electrical model of the line. This is an instance of the - :class:`LineParameters` class and can be used by multiple lines. + Parameters defining the electric model of the line using its impedance and shunt + admittance matrices. This is an instance of the :class:`LineParameters` class and + can be used by multiple lines. length: - The length of the line in km. + The length of the line (in km). phases: The phases of the line. A string like ``"abc"`` or ``"an"`` etc. The order of the @@ -256,6 +257,7 @@ def phases(self) -> str: @property @ureg_wraps("km", (None,)) def length(self) -> Q_[float]: + """The length of the line (in km).""" return self._length @length.setter @@ -274,7 +276,7 @@ def length(self, value: float | Q_[float]) -> None: @property def parameters(self) -> LineParameters: - """The parameters of the line.""" + """The parameters defining the impedance and shunt admittance matrices of line model.""" return self._parameters @parameters.setter @@ -320,18 +322,18 @@ def parameters(self, value: LineParameters) -> None: @property @ureg_wraps("ohm", (None,)) def z_line(self) -> Q_[ComplexArray]: - """Impedance of the line in Ohm""" + """Impedance of the line (in Ohm).""" return self.parameters._z_line * self._length @property @ureg_wraps("S", (None,)) def y_shunt(self) -> Q_[ComplexArray]: - """Shunt admittance of the line in Siemens""" + """Shunt admittance of the line (in Siemens).""" return self.parameters._y_shunt * self._length @property def max_current(self) -> Q_[float] | None: - """The maximum current loading of the line in A.""" + """The maximum current loading of the line (in A).""" # Do not add a setter. The user must know that if they change the max_current, it changes # for all lines that share the parameters. It is better to set it on the parameters. return self.parameters.max_current @@ -353,7 +355,7 @@ def _res_series_currents_getter(self, warning: bool) -> ComplexArray: @property @ureg_wraps("A", (None,)) def res_series_currents(self) -> Q_[ComplexArray]: - """Get the current in the series elements of the line (A).""" + """Get the current in the series elements of the line (in A).""" return self._res_series_currents_getter(warning=True) def _res_series_power_losses_getter(self, warning: bool) -> ComplexArray: @@ -363,7 +365,7 @@ def _res_series_power_losses_getter(self, warning: bool) -> ComplexArray: @property @ureg_wraps("VA", (None,)) def res_series_power_losses(self) -> Q_[ComplexArray]: - """Get the power losses in the series elements of the line (VA).""" + """Get the power losses in the series elements of the line (in VA).""" return self._res_series_power_losses_getter(warning=True) def _res_shunt_values_getter(self, warning: bool) -> tuple[ComplexArray, ComplexArray, ComplexArray, ComplexArray]: @@ -387,7 +389,7 @@ def _res_shunt_currents_getter(self, warning: bool) -> tuple[ComplexArray, Compl @property @ureg_wraps(("A", "A"), (None,)) def res_shunt_currents(self) -> tuple[Q_[ComplexArray], Q_[ComplexArray]]: - """Get the currents in the shunt elements of the line (A).""" + """Get the currents in the shunt elements of the line (in A).""" return self._res_shunt_currents_getter(warning=True) def _res_shunt_power_losses_getter(self, warning: bool) -> ComplexArray: @@ -399,7 +401,7 @@ def _res_shunt_power_losses_getter(self, warning: bool) -> ComplexArray: @property @ureg_wraps("VA", (None,)) def res_shunt_power_losses(self) -> Q_[ComplexArray]: - """Get the power losses in the shunt elements of the line (VA).""" + """Get the power losses in the shunt elements of the line (in VA).""" return self._res_shunt_power_losses_getter(warning=True) def _res_power_losses_getter(self, warning: bool) -> ComplexArray: @@ -410,7 +412,7 @@ def _res_power_losses_getter(self, warning: bool) -> ComplexArray: @property @ureg_wraps("VA", (None,)) def res_power_losses(self) -> Q_[ComplexArray]: - """Get the power losses in the line (VA).""" + """Get the power losses in the line (in VA).""" return self._res_power_losses_getter(warning=True) @property diff --git a/roseau/load_flow/models/lines/parameters.py b/roseau/load_flow/models/lines/parameters.py index 887cb885..f5c60061 100644 --- a/roseau/load_flow/models/lines/parameters.py +++ b/roseau/load_flow/models/lines/parameters.py @@ -1,5 +1,7 @@ import logging import re +from importlib import resources +from pathlib import Path from typing import NoReturn import numpy as np @@ -11,7 +13,6 @@ from roseau.load_flow.typing import ComplexArray, ComplexArrayLike2D, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow.utils import ( - CX, EPSILON_0, EPSILON_R, MU_0, @@ -19,6 +20,7 @@ PI, RHO, TAN_D, + CatalogueMixin, ConductorType, Identifiable, InsulatorType, @@ -28,24 +30,38 @@ logger = logging.getLogger(__name__) +_DEFAULT_CONDUCTOR_TYPE = { + LineType.OVERHEAD: ConductorType.ACSR, + LineType.TWISTED: ConductorType.AL, + LineType.UNDERGROUND: ConductorType.AL, +} -class LineParameters(Identifiable, JsonMixin): +_DEFAULT_INSULATION_TYPE = { + LineType.OVERHEAD: InsulatorType.UNKNOWN, # Not used for overhead lines + LineType.TWISTED: InsulatorType.XLPE, + LineType.UNDERGROUND: InsulatorType.PVC, +} + + +class LineParameters(Identifiable, JsonMixin, CatalogueMixin[pd.DataFrame]): """Parameters that define electrical models of lines.""" - _type_re = "|".join("|".join(x) for x in LineType.CODES.values()) + _type_re = "|".join(x.code() for x in LineType) _material_re = "|".join(x.code() for x in ConductorType) _section_re = r"[1-9][0-9]*" - _REGEXP_LINE_TYPE_NAME: re.Pattern = re.compile( - rf"^({_type_re})_({_material_re})_{_section_re}$", flags=re.IGNORECASE - ) + _REGEXP_LINE_TYPE_NAME = re.compile(rf"^({_type_re})_({_material_re})_{_section_re}$", flags=re.IGNORECASE) - @ureg_wraps(None, (None, None, "ohm/km", "S/km", "A")) + @ureg_wraps(None, (None, None, "ohm/km", "S/km", "A", None, None, None, "mm²")) def __init__( self, id: Id, z_line: ComplexArrayLike2D, y_shunt: ComplexArrayLike2D | None = None, max_current: float | None = None, + line_type: LineType | None = None, + conductor_type: ConductorType | None = None, + insulator_type: InsulatorType | None = None, + section: float | Q_[float] | None = None, ) -> None: """LineParameters constructor. @@ -60,7 +76,27 @@ def __init__( The Y matrix of the line (Siemens/km). This field is optional if the line has no shunt part. max_current: - An optional maximum current loading of the line (A). It is not used in the load flow. + The maximum current loading of the line (A). The maximum current is optional, it is + not used in the load flow but can be used to check for overloading. + See also :meth:`Line.res_violated `. + + line_type: + The type of the line (overhead, underground, twisted). The line type is optional, + it is informative only and is not used in the load flow. This field gets + automatically filled when the line parameters are created from a geometric model or + from the catalogue. + + conductor_type: + The type of the conductor material (Aluminum, Copper, ...). The conductor type is + optional, it is informative only and is not used in the load flow. This field gets + automatically filled when the line parameters are created from a geometric model or + from the catalogue. + + insulator_type: + The type of the cable insulator (PVC, XLPE, ...). The insulator type is optional, + it is informative only and is not used in the load flow. This field gets + automatically filled when the line parameters are created from a geometric model or + from the catalogue. """ super().__init__(id) self._z_line = np.array(z_line, dtype=np.complex128) @@ -71,6 +107,10 @@ def __init__( self._with_shunt = not np.allclose(y_shunt, 0) self._y_shunt = np.array(y_shunt, dtype=np.complex128) self.max_current = max_current + self._line_type = line_type + self._conductor_type = conductor_type + self._insulator_type = insulator_type + self._section: float = section self._check_matrix() def __eq__(self, other: object) -> bool: @@ -110,6 +150,26 @@ def max_current(self) -> Q_[float] | None: """The maximum current loading of the line (A) if it is set.""" return None if self._max_current is None else Q_(self._max_current, "A") + @property + def line_type(self) -> LineType | None: + """The type of the line. Informative only, it has no impact on the load flow.""" + return self._line_type + + @property + def conductor_type(self) -> ConductorType | None: + """The type of the conductor material. Informative only, it has no impact on the load flow.""" + return self._conductor_type + + @property + def insulator_type(self) -> InsulatorType | None: + """The type of the cable insulator. Informative only, it has no impact on the load flow.""" + return self._insulator_type + + @property + def section(self) -> Q_[float] | None: + """The cross section area of the cable (in mm²). Informative only, it has no impact on the load flow.""" + return None if self._section is None else Q_(self._section, "mm**2") + @max_current.setter @ureg_wraps(None, (None, "A")) def max_current(self, value: float | Q_[float] | None) -> None: @@ -299,11 +359,12 @@ def _sym_to_zy( def from_geometry( cls, id: Id, + *, line_type: LineType, - conductor_type: ConductorType, - insulator_type: InsulatorType, + conductor_type: ConductorType | None = None, + insulator_type: InsulatorType | None = None, section: float | Q_[float], - section_neutral: float | Q_[float], + section_neutral: float | Q_[float] | None = None, height: float | Q_[float], external_diameter: float | Q_[float], max_current: float | Q_[float] | None = None, @@ -315,25 +376,29 @@ def from_geometry( The id of the line parameters type. line_type: - Overhead or underground. + Overhead or underground. See also :class:`~roseau.load_flow.LineType`. conductor_type: - Type of the conductor + Type of the conductor. If ``None``, ``ACSR`` is used for overhead lines and ``AL`` + for underground or twisted lines. See also :class:`~roseau.load_flow.ConductorType`. insulator_type: - Type of insulator. + Type of insulator. If ``None``, ``XLPE`` is used for twisted lines and ``PVC`` for + underground lines. See also :class:`~roseau.load_flow.InsulatorType`. section: - Surface of the phases (mm²). + Cross-section surface area of the phases (mm²). section_neutral: - Surface of the neutral (mm²). + Cross-section surface area of the neutral (mm²). If None it will be the same as the + section of the other phases. height: - Height of the line (m). + Height of the line (m). It must be positive for overhead lines and negative for + underground lines. external_diameter: - External diameter of the wire (m). + External diameter of the cable (m). max_current: An optional maximum current loading of the line (A). It is not used in the load flow. @@ -344,7 +409,7 @@ def from_geometry( See Also: :ref:`Line parameters alternative constructor documentation ` """ - z_line, y_shunt = cls._geometry_to_zy( + z_line, y_shunt, line_type, conductor_type, insulator_type, section = cls._from_geometry( id=id, line_type=line_type, conductor_type=conductor_type, @@ -354,30 +419,39 @@ def from_geometry( height=height, external_diameter=external_diameter, ) - return cls(id=id, z_line=z_line, y_shunt=y_shunt, max_current=max_current) + return cls( + id=id, + z_line=z_line, + y_shunt=y_shunt, + max_current=max_current, + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=section, + ) @staticmethod - def _geometry_to_zy( + def _from_geometry( id: Id, line_type: LineType, - conductor_type: ConductorType, - insulator_type: InsulatorType, + conductor_type: ConductorType | None, + insulator_type: InsulatorType | None, section: float, - section_neutral: float, + section_neutral: float | None, height: float, external_diameter: float, - ) -> tuple[ComplexArray, ComplexArray]: - """Create impedance and admittance matrix using a geometric model. + ) -> tuple[ComplexArray, ComplexArray, LineType, ConductorType, InsulatorType, float]: + """Create impedance and admittance matrices using a geometric model. Args: id: The id of the line parameters. line_type: - Overhead or underground. + Overhead, twisted overhead, or underground. conductor_type: - Type of the conductor + Type of the conductor material (Aluminum, Copper, ...). insulator_type: Type of insulator. @@ -386,10 +460,12 @@ def _geometry_to_zy( Surface of the phases (mm²). section_neutral: - Surface of the neutral (mm²). + Surface of the neutral (mm²). If None it will be the same as the section of the + other phases. height: - Height of the line (m). + Height of the line (m). Positive for overhead lines and negative for underground + lines. external_diameter: External diameter of the wire (m). @@ -401,53 +477,87 @@ def _geometry_to_zy( # dpn = data["dpn"] # Distance phase to neutral (m) # dsh = data["dsh"] # Diameter of the sheath (mm) + if conductor_type is None: + conductor_type = _DEFAULT_CONDUCTOR_TYPE[line_type] + if insulator_type is None: + insulator_type = _DEFAULT_INSULATION_TYPE[line_type] + if section_neutral is None: + section_neutral = section + line_type = LineType(line_type) + conductor_type = ConductorType(conductor_type) + insulator_type = InsulatorType(insulator_type) + # Geometric configuration if line_type in (LineType.OVERHEAD, LineType.TWISTED): # TODO This configuration is for twisted lines... Create a overhead configuration. - # TODO Add some checks on provided geometric values... + if height <= 0: + msg = f"The height of a '{line_type}' line must be a positive number." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + x = np.sqrt(3) * external_diameter / 8 coord = np.array( [ - [-np.sqrt(3) / 8 * external_diameter, height + external_diameter / 8], - [np.sqrt(3) / 8 * external_diameter, height + external_diameter / 8], + [-x, height + external_diameter / 8], + [x, height + external_diameter / 8], [0, height - external_diameter / 4], [0, height], ] ) # m coord_prim = np.array( [ - [-np.sqrt(3) / 8 * external_diameter, -height - external_diameter / 8], - [np.sqrt(3) / 8 * external_diameter, -height - external_diameter / 8], + [-x, -height - external_diameter / 8], + [x, -height - external_diameter / 8], [0, -height + external_diameter / 4], [0, -height], ] ) # m epsilon = EPSILON_0.m_as("F/m") elif line_type == LineType.UNDERGROUND: - coord = np.array( - [ - [-np.sqrt(2) / 8 * external_diameter, height - np.sqrt(2) / 8 * external_diameter], - [np.sqrt(2) / 8 * external_diameter, height - np.sqrt(2) / 8 * external_diameter], - [np.sqrt(2) / 8 * external_diameter, height + np.sqrt(2) / 8 * external_diameter], - [-np.sqrt(2) / 8 * external_diameter, height + np.sqrt(2) / 8 * external_diameter], - ] - ) # m - coord_prim = np.array( - [ - [-np.sqrt(2) * 3 / 8 * external_diameter, height - np.sqrt(2) * 3 / 8 * external_diameter], - [np.sqrt(2) * 3 / 8 * external_diameter, height - np.sqrt(2) * 3 / 8 * external_diameter], - [np.sqrt(2) * 3 / 8 * external_diameter, height + np.sqrt(2) * 3 / 8 * external_diameter], - [-np.sqrt(2) * 3 / 8 * external_diameter, height + np.sqrt(2) * 3 / 8 * external_diameter], - ] - ) # m + if height >= 0: + msg = f"The height of a '{line_type}' line must be a negative number." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + x = np.sqrt(2) * external_diameter / 8 + coord = np.array([[-x, height - x], [x, height - x], [x, height + x], [-x, height + x]]) # m + xp = x * 3 + coord_prim = np.array([[-xp, height - xp], [xp, height - xp], [xp, height + xp], [-xp, height + xp]]) # m epsilon = (EPSILON_0 * EPSILON_R[insulator_type]).m_as("F/m") else: - msg = f"The line type of the line {id!r} is unknown. It should have been filled in the reading." + msg = f"The line type {line_type!r} of the line {id!r} is unknown." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE) # Distance computation sections = np.array([section, section, section, section_neutral], dtype=np.float64) * 1e-6 # surfaces (m2) radius = np.sqrt(sections / PI) # radius (m) + phase_radius, neutral_radius = radius[0], radius[3] + if line_type == LineType.TWISTED: + max_radii = external_diameter / 4 + if phase_radius + neutral_radius > max_radii: + msg = ( + f"Conductors too big for 'twisted' line parameter of id {id!r}. Inequality " + f"`neutral_radius + phase_radius <= external_diameter / 4` is not satisfied." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + elif line_type == LineType.UNDERGROUND: + max_radii = external_diameter / 4 * np.sqrt(2) + if phase_radius + neutral_radius > max_radii: + msg = ( + f"Conductors too big for 'underground' line parameter of id {id!r}. Inequality " + f"`neutral_radius + phase_radius <= external_diameter * sqrt(2) / 4` is not satisfied." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + if phase_radius * 2 > max_radii: + msg = ( + f"Conductors too big for 'underground' line parameter of id {id!r}. Inequality " + f"`phase_radius*2 <= external_diameter * sqrt(2) / 4` is not satisfied." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + else: + pass # TODO Overhead lines check gmr = radius * np.exp(-0.25) # geometric mean radius (m) # distance between two wires (m) coord_new_dim = coord[:, None, :] @@ -488,7 +598,7 @@ def _geometry_to_zy( y_shunt[mask_diagonal] = np.einsum("ij->i", y) y_shunt[mask_off_diagonal] = -y[mask_off_diagonal] - return z_line, y_shunt + return z_line, y_shunt, line_type, conductor_type, insulator_type, section @classmethod @deprecated( @@ -539,8 +649,8 @@ def from_name_lv( # Check the user input and retrieve enumerated types line_type, conductor_type, section = name.split("_") - line_type = LineType.from_string(line_type) - conductor_type = ConductorType.from_string(conductor_type) + line_type = LineType(line_type) + conductor_type = ConductorType(conductor_type) insulator_type = InsulatorType.PVC section = float(section) @@ -565,13 +675,19 @@ def from_name_lv( ) @classmethod + # @deprecated( + # "The method LineParameters.from_name_mv() is deprecated and will be removed in a future " + # "version. Use LineParameters.from_catalogue() instead.", + # category=FutureWarning, + # ) @ureg_wraps(None, (None, None, "A")) def from_name_mv(cls, name: str, max_current: float | Q_[float] | None = None) -> Self: - """Method to get the electrical parameters of a MV line from its canonical name. + """Get the electrical parameters of a MV line from its canonical name (France specific model) Args: name: - The name of the line the parameters must be computed. E.g. "U_AL_150". + The canonical name of the line parameters. It must be in the format + `lineType_conductorType_crossSection`. E.g. "U_AL_150". max_current: An optional maximum current loading of the line (A). It is not used in the load flow. @@ -587,26 +703,31 @@ def from_name_mv(cls, name: str, max_current: float | Q_[float] | None = None) - # Check the user input and retrieve enumerated types line_type, conductor_type, section = name.split("_") - line_type = LineType.from_string(string=line_type) - conductor_type = ConductorType.from_string(conductor_type) + line_type = LineType(line_type) + conductor_type = ConductorType(conductor_type) section = Q_(float(section), "mm**2") r = RHO[conductor_type] / section - x = CX[line_type] - if type == LineType.OVERHEAD: + if line_type == LineType.OVERHEAD: c_b1 = Q_(50, "µF/km") c_b2 = Q_(0, "µF/(km*mm**2)") - elif type == LineType.TWISTED: - # Twisted line + x = Q_(0.35, "ohm/km") + elif line_type == LineType.TWISTED: c_b1 = Q_(1750, "µF/km") c_b2 = Q_(5, "µF/(km*mm**2)") - else: + x = Q_(0.1, "ohm/km") + elif line_type == LineType.UNDERGROUND: if section <= Q_(50, "mm**2"): c_b1 = Q_(1120, "µF/km") c_b2 = Q_(33, "µF/(km*mm**2)") else: c_b1 = Q_(2240, "µF/km") c_b2 = Q_(15, "µF/(km*mm**2)") + x = Q_(0.1, "ohm/km") + else: + msg = f"The line type {line_type!r} of the line {name!r} is unknown." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE) b = (c_b1 + c_b2 * section) * 1e-4 * OMEGA b = b.to("S/km") @@ -614,6 +735,233 @@ def from_name_mv(cls, name: str, max_current: float | Q_[float] | None = None) - y_shunt = b * 1j * np.eye(3, dtype=np.float64) # in siemens/km return cls(name, z_line=z_line, y_shunt=y_shunt, max_current=max_current) + # + # Catalogue Mixin + # + @classmethod + def catalogue_path(cls) -> Path: + return Path(resources.files("roseau.load_flow") / "data" / "lines").expanduser().absolute() + + @classmethod + def catalogue_data(cls) -> pd.DataFrame: + file = cls.catalogue_path() / "Catalogue.csv" + return pd.read_csv(file, parse_dates=False).fillna({"insulator": ""}) + + @classmethod + def _get_catalogue( + cls, + name: str | re.Pattern[str] | None, + line_type: str | None, + conductor_type: str | None, + insulator_type: str | None, + section: float | None, + raise_if_not_found: bool, + ) -> tuple[pd.DataFrame, str]: + catalogue_data = cls.catalogue_data() + + # Filter on strings/regular expressions + query_msg_list = [] + for value, column_name, display_name, display_name_plural in [ + (name, "name", "name", "names"), + ]: + if value is None: + continue + + mask = cls._filter_catalogue_str(value, strings=catalogue_data[column_name]) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=repr(value), + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name], + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r}") + + # Filter on enumerated types + for value, column_name, display_name, enum_class in ( + (line_type, "type", "line_type", LineType), + (conductor_type, "material", "conductor_type", ConductorType), + (insulator_type, "insulator", "insulator_type", InsulatorType), + ): + if value is None: + continue + + enum_series = catalogue_data[column_name].apply(enum_class) + try: + mask = enum_series == enum_class(value) + except RoseauLoadFlowException: + mask = pd.Series(False, index=catalogue_data.index) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=repr(value), + name=display_name, + name_plural=display_name + "s", + strings=enum_series, + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r}") + + # Filter on floats + for value, column_name, display_name, display_name_plural, unit in [ + (section, "section", "cross-section", "cross-sections", "mm²"), + ]: + if value is None: + continue + + mask = np.isclose(catalogue_data[column_name], value) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=f"{value:.1f} {unit}", + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name].apply(lambda x: f"{x:.1f} {unit}"), # noqa: B023 + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r} {unit}") + + return catalogue_data, ", ".join(query_msg_list) + + @classmethod + @ureg_wraps(None, (None, None, None, None, None, "mm²", None)) + def from_catalogue( + cls, + name: str | re.Pattern[str] | None = None, + line_type: str | None = None, + conductor_type: str | None = None, + insulator_type: str | None = None, + section: float | Q_[float] | None = None, + id: Id | None = None, + ) -> Self: + """Create line parameters from a catalogue. + + Args: + name: + The name of the line parameters to get from the catalogue. It can be a regular + expression. + + line_type: + The type of the line parameters to get. It can be ``"overhead"``, ``"twisted"``, or + ``"underground"``. See also :class:`~roseau.load_flow.LineType`. + + conductor_type: + The type of the conductor material (Al, Cu, ...). See also + :class:`~roseau.load_flow.ConductorType`. + + insulator_type: + The type of insulator. See also :class:`~roseau.load_flow.InsulatorType`. + + section: + The cross-section surface area of the phases (mm²). + + id: + A unique ID for the created line parameters object (optional). If ``None`` + (default), the id of the created object will be its name in the catalogue. + + Returns: + The created line parameters. + """ + catalogue_data, query_info = cls._get_catalogue( + name=name, + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=section, + raise_if_not_found=True, + ) + + cls._assert_one_found( + found_data=catalogue_data["name"].tolist(), display_name="line parameters", query_info=query_info + ) + idx = catalogue_data.index[0] + name = str(catalogue_data.at[idx, "name"]) + r = catalogue_data.at[idx, "r"] + x = catalogue_data.at[idx, "x"] + b = catalogue_data.at[idx, "b"] + line_type = LineType(catalogue_data.at[idx, "type"]) + conductor_type = ConductorType(catalogue_data.at[idx, "material"]) + insulator_type = InsulatorType(catalogue_data.at[idx, "insulator"]) + section = catalogue_data.at[idx, "section"] + max_current = catalogue_data.at[idx, "maximal_current"] + if pd.isna(max_current): + max_current = None + z_line = (r + x * 1j) * np.eye(3, dtype=np.complex128) + y_shunt = (b * 1j) * np.eye(3, dtype=np.complex128) + if id is None: + id = name + return cls( + id=id, + z_line=z_line, + y_shunt=y_shunt, + max_current=max_current, + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=section, + ) + + @classmethod + @ureg_wraps(None, (None, None, None, None, None, "mm²")) + def get_catalogue( + cls, + name: str | re.Pattern[str] | None = None, + line_type: str | None = None, + conductor_type: str | None = None, + insulator_type: str | None = None, + section: float | Q_[float] | None = None, + ) -> pd.DataFrame: + """Get the catalogue of available lines. + + You can use the parameters below to filter the catalogue. If you do not specify any + parameter, all the catalogue will be returned. + + Args: + name: + The name of the line parameters to get from the catalogue. It can be a regular + expression. + + line_type: + The type of the line parameters to get. It can be ``"overhead"``, ``"twisted"``, or + ``"underground"``. See also :class:`~roseau.load_flow.LineType`. + + conductor_type: + The type of the conductor material (Al, Cu, ...). See also + :class:`~roseau.load_flow.ConductorType`. + + insulator_type: + The type of insulator. See also :class:`~roseau.load_flow.InsulatorType`. + + section: + The cross-section surface area of the phases (mm²). + + Returns: + The catalogue data as a dataframe. + """ + catalogue_data, _ = cls._get_catalogue( + name=name, + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=section, + raise_if_not_found=False, + ) + return catalogue_data.rename( + columns={ + "name": "Name", + "r": "Resistance (ohm/km)", + "x": "Reactance (ohm/km)", + "b": "Susceptance (µS/km)", + "maximal_current": "Maximal current (A)", + "type": "Line type", + "material": "Conductor material", + "insulator": "Insulator type", + "section": "Cross-section (mm²)", + } + ).set_index("Name") + # # Json Mixin interface # @@ -630,7 +978,19 @@ def from_dict(cls, data: JsonDict) -> Self: """ z_line = np.array(data["z_line"][0]) + 1j * np.array(data["z_line"][1]) y_shunt = np.array(data["y_shunt"][0]) + 1j * np.array(data["y_shunt"][1]) if "y_shunt" in data else None - return cls(id=data["id"], z_line=z_line, y_shunt=y_shunt, max_current=data.get("max_current")) + line_type = LineType(data["line_type"]) if "line_type" in data else None + conductor_type = ConductorType(data["conductor_type"]) if "conductor_type" in data else None + insulator_type = InsulatorType(data["insulator_type"]) if "insulator_type" in data else None + return cls( + id=data["id"], + z_line=z_line, + y_shunt=y_shunt, + max_current=data.get("max_current"), + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=data.get("section"), + ) def to_dict(self, *, _lf_only: bool = False) -> JsonDict: """Return the line parameters information as a dictionary format.""" @@ -639,6 +999,14 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: res["y_shunt"] = [self._y_shunt.real.tolist(), self._y_shunt.imag.tolist()] if not _lf_only and self.max_current is not None: res["max_current"] = self.max_current.magnitude + if not _lf_only and self._line_type is not None: + res["line_type"] = self._line_type.name + if not _lf_only and self._conductor_type is not None: + res["conductor_type"] = self._conductor_type.name + if not _lf_only and self._insulator_type is not None: + res["insulator_type"] = self._insulator_type.name + if not _lf_only and self._section is not None: + res["section"] = self._section return res def _results_to_dict(self, warning: bool) -> NoReturn: diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index 4ad0b40c..9178347a 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -125,9 +125,7 @@ def _validate_value(self, value: ComplexArrayLike1D) -> ComplexArray: if len(value) != self._size: msg = f"Incorrect number of {self._type}s: {len(value)} instead of {self._size}" logger.error(msg) - raise RoseauLoadFlowException( - msg=msg, code=RoseauLoadFlowExceptionCode.from_string(f"BAD_{self._symbol}_SIZE") - ) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode[f"BAD_{self._symbol}_SIZE"]) # A load cannot have any zero impedance if self._type == "impedance" and np.isclose(value, 0).any(): msg = f"An impedance of the load {self.id!r} is null" diff --git a/roseau/load_flow/models/tests/test_line_parameters.py b/roseau/load_flow/models/tests/test_line_parameters.py index 85b517b7..fc5c77c6 100644 --- a/roseau/load_flow/models/tests/test_line_parameters.py +++ b/roseau/load_flow/models/tests/test_line_parameters.py @@ -1,6 +1,9 @@ +import re + import numpy as np import numpy.linalg as nplin import numpy.testing as npt +import pandas as pd import pytest from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode @@ -112,7 +115,7 @@ def test_geometry(): # line_data = {"dpp": 0, "dpn": 0, "dsh": 0.04} # Working example - z_line, y_shunt = LineParameters._geometry_to_zy( + z_line, y_shunt, line_type, conductor_type, insulator_type, section = LineParameters._from_geometry( "test", line_type=LineType.OVERHEAD, conductor_type=ConductorType.AL, @@ -123,6 +126,7 @@ def test_geometry(): external_diameter=0.04, ) + # TODO regenerate all expected values with the IEC constants and update this test y_line_expected = np.array( [ [3.3915102901533754, -1.2233003903972888, -1.2233003903972615, -0.7121721195595286], @@ -139,7 +143,7 @@ def test_geometry(): ] ) - npt.assert_allclose(z_line, nplin.inv(y_line_expected)) + npt.assert_allclose(z_line, nplin.inv(y_line_expected), rtol=0.04, atol=0.02) y_shunt_expected = np.array( [ [ @@ -168,12 +172,17 @@ def test_geometry(): ], ] ) - npt.assert_allclose(y_shunt, y_shunt_expected) + npt.assert_allclose(y_shunt, y_shunt_expected, rtol=0.001) + + assert line_type == LineType.OVERHEAD + assert conductor_type == ConductorType.AL + assert insulator_type == InsulatorType.PEX + assert section == 150 # line_data = {"dpp": 0, "dpn": 0, "dsh": 0.04} # Working example - z_line, y_shunt = LineParameters._geometry_to_zy( + z_line, y_shunt, line_type, conductor_type, insulator_type, section = LineParameters._from_geometry( "test", line_type=LineType.UNDERGROUND, conductor_type=ConductorType.AL, @@ -198,7 +207,7 @@ def test_geometry(): [-0.03859093131793137, 0.20837873067712717, -0.03859093131792582, -0.6182914857776997], ] ) - npt.assert_allclose(z_line, nplin.inv(y_line_expected)) + npt.assert_allclose(z_line, nplin.inv(y_line_expected), rtol=0.04, atol=0.02) y_shunt_expected = np.array( [ [ @@ -228,7 +237,12 @@ def test_geometry(): ] ) - npt.assert_allclose(y_shunt, y_shunt_expected) + npt.assert_allclose(y_shunt, y_shunt_expected, rtol=0.3) + + assert line_type == LineType.UNDERGROUND + assert conductor_type == ConductorType.AL + assert insulator_type == InsulatorType.PVC + assert section == 150 def test_sym(): @@ -315,39 +329,155 @@ def test_sym(): def test_from_name_lv(): with pytest.raises(RoseauLoadFlowException) as e, pytest.warns(FutureWarning): - LineParameters.from_name_lv("totoS_Al_150") + LineParameters.from_name_lv("totoU_Al_150") assert "The line type name does not follow the syntax rule." in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.BAD_TYPE_NAME_SYNTAX with pytest.warns(FutureWarning): - lp = LineParameters.from_name_lv("S_AL_150") + lp = LineParameters.from_name_lv("U_AL_150") assert lp.z_line.shape == (4, 4) assert lp.y_shunt.shape == (4, 4) assert (lp.z_line.real >= 0).all().all() - with pytest.warns(FutureWarning): - lp2 = LineParameters.from_name_lv("U_AL_150") - npt.assert_allclose(lp2.z_line.m_as("ohm/km"), lp.z_line.m_as("ohm/km")) - npt.assert_allclose(lp2.y_shunt.m_as("S/km"), lp.y_shunt.m_as("S/km"), rtol=1e-4) - def test_from_name_mv(): - with pytest.raises(RoseauLoadFlowException) as e: - LineParameters.from_name_mv("totoS_Al_150") + with pytest.raises(RoseauLoadFlowException) as e, pytest.warns(FutureWarning): + LineParameters.from_name_mv("totoU_Al_150") assert "The line type name does not follow the syntax rule." in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.BAD_TYPE_NAME_SYNTAX - lp = LineParameters.from_name_mv("S_AL_150") - z_line_expected = (0.188 + 0.1j) * np.eye(3) + lp = LineParameters.from_name_mv("U_AL_150") + z_line_expected = (0.1767 + 0.1j) * np.eye(3) y_shunt_expected = 0.00014106j * np.eye(3) - npt.assert_allclose(lp.z_line.m_as("ohm/km"), z_line_expected) - npt.assert_allclose(lp.y_shunt.m_as("S/km"), y_shunt_expected, rtol=1e-4) - - # The same with "underground" - lp = LineParameters.from_name_mv("U_AL_150") - npt.assert_allclose(lp.z_line.m_as("ohm/km"), z_line_expected) - npt.assert_allclose(lp.y_shunt.m_as("S/km"), y_shunt_expected, rtol=1e-4) + npt.assert_allclose(lp.z_line.m_as("ohm/km"), z_line_expected, rtol=0.01, atol=0.01) + npt.assert_allclose(lp.y_shunt.m_as("S/km"), y_shunt_expected, rtol=0.01, atol=0.01) + + +def test_catalogue_data(): + # The catalogue data path exists + catalogue_path = LineParameters.catalogue_path() + assert catalogue_path.exists() + + catalogue_data = LineParameters.catalogue_data() + + # Check that the name is unique + assert catalogue_data["name"].is_unique, "Regenerate catalogue." + + for row in catalogue_data.itertuples(): + assert re.match(r"^(?:U|O|T)_[A-Z]+_\d+(?:_\w+)?$", row.name) + assert isinstance(row.r, float) + assert isinstance(row.x, float) + assert isinstance(row.b, float) + assert isinstance(row.maximal_current, int | float) + LineType(row.type) # Check that the type is valid + ConductorType(row.material) # Check that the material is valid + InsulatorType(row.insulator) # Check that the insulator is valid + assert isinstance(row.section, int | float) + + +def test_from_catalogue(): + # Unknown strings + for field_name in ("name",): + # String + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: "unknown"}) + assert e.value.msg.startswith(f"No {field_name} matching 'unknown' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Regexp + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: r"unknown[a-z]+"}) + assert e.value.msg.startswith(f"No {field_name} matching 'unknown[a-z]+' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Unknown enums + for field_name in ("line_type", "conductor_type", "insulator_type"): + # String + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: "invalid"}) + assert e.value.msg.startswith(f"No {field_name} matching 'invalid' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Regexp + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: r"invalid[a-z]+"}) + assert e.value.msg.startswith(f"No {field_name} matching 'invalid[a-z]+' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Unknown floats + for field_name, display_name, display_unit in (("section", "cross-section", "mm²"),): + # Without unit + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: 3.1415}) + assert e.value.msg.startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # With unit + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: Q_(0.031415, "cm²")}) + assert e.value.msg.startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Several line parameters + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(name=r"U_AL_") + assert e.value.msg == ( + "Several line parameters matching the query (name='U_AL_') have been found: " + "'U_AL_19', 'U_AL_20', 'U_AL_22', 'U_AL_25', 'U_AL_28', 'U_AL_29', 'U_AL_33', " + "'U_AL_34', 'U_AL_37', 'U_AL_38', 'U_AL_40', 'U_AL_43', 'U_AL_48', 'U_AL_50', " + "'U_AL_54', 'U_AL_55', 'U_AL_59', 'U_AL_60', 'U_AL_69', 'U_AL_70', 'U_AL_74', " + "'U_AL_75', 'U_AL_79', 'U_AL_80', 'U_AL_90', 'U_AL_93', 'U_AL_95', 'U_AL_100', " + "'U_AL_116', 'U_AL_117', 'U_AL_120', 'U_AL_147', 'U_AL_148', 'U_AL_150', 'U_AL_228', " + "'U_AL_240', 'U_AL_288'." + ) + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + + # Success + lp = LineParameters.from_catalogue(name="U_AL_150") + assert lp.id == "U_AL_150" + assert lp.z_line.shape == (3, 3) + assert lp.y_shunt.shape == (3, 3) + assert lp.max_current > 0 + assert lp.line_type == LineType.UNDERGROUND + assert lp.conductor_type == ConductorType.AL + assert lp.insulator_type == InsulatorType.UNKNOWN + assert lp.section.m == 150 + + lp = LineParameters.from_catalogue(name="U_AL_150", id="lp1") + assert lp.id == "lp1" + + +def test_get_catalogue(): + # Get the entire catalogue + catalogue = LineParameters.get_catalogue() + assert isinstance(catalogue, pd.DataFrame) + assert catalogue.shape == (355, 8) + + # Filter on a single attribute + for field_name, value, expected_size in ( + ("name", r"U_AL_150.*", 1), + ("line_type", "OvErHeAd", 122), + ("conductor_type", "Cu", 121), + # ("insulator_type", InsulatorType.SE, 240), + ("section", 150, 9), + ("section", Q_(1.5, "cm²"), 9), + ): + filtered_catalogue = LineParameters.get_catalogue(**{field_name: value}) + assert filtered_catalogue.shape == (expected_size, 8) + + # Filter on two attributes + for field_name, value, expected_size in ( + ("name", r"U_AL_150.*", 1), + ("line_type", "OvErHeAd", 122), + ("section", 150, 9), + ): + filtered_catalogue = LineParameters.get_catalogue(**{field_name: value}) + assert filtered_catalogue.shape == (expected_size, 8) + + # No results + empty_catalogue = LineParameters.get_catalogue(section=15000) + assert empty_catalogue.shape == (0, 8) def test_max_current(): diff --git a/roseau/load_flow/models/tests/test_transformer_parameters.py b/roseau/load_flow/models/tests/test_transformer_parameters.py index 3b259f6d..8f1bf8ea 100644 --- a/roseau/load_flow/models/tests/test_transformer_parameters.py +++ b/roseau/load_flow/models/tests/test_transformer_parameters.py @@ -1,13 +1,13 @@ import numbers import numpy as np +import pandas as pd import pytest from pint import DimensionalityError from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.models import TransformerParameters from roseau.load_flow.units import Q_ -from roseau.load_flow.utils import console def test_transformer_parameters(): @@ -302,7 +302,7 @@ def test_transformer_type(): else: with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.extract_windings(t) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS else: with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.extract_windings(t) @@ -368,16 +368,14 @@ def test_from_catalogue(): # String with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(**{field_name: "unknown"}) - assert e.value.args[0].startswith(f"No {field_name} matching the name 'unknown' has been found. Available ") - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.msg.startswith(f"No {field_name} matching 'unknown' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Regexp with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(**{field_name: r"unknown[a-z]+"}) - assert e.value.args[0].startswith( - f"No {field_name} matching the name 'unknown[a-z]+' has been found. " f"Available " - ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.msg.startswith(f"No {field_name} matching 'unknown[a-z]+' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Unknown floats for field_name, display_name, display_unit in ( @@ -388,78 +386,76 @@ def test_from_catalogue(): # Without unit with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(**{field_name: 3141.5}) - assert e.value.args[0].startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.msg.startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # With unit with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(**{field_name: Q_(3141.5, display_unit.removeprefix("k"))}) - assert e.value.args[0].startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.msg.startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Several transformers with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(type="yzn", sn=50e3) - assert ( - e.value.args[0] - == "Several transformers matching the query (\"type='yzn', nominal power=50.0 kVA\") have been found. Please " - "look at the catalogue using the `print_catalogue` class method." + assert e.value.msg == ( + "Several transformers matching the query (type='yzn', nominal power=50.0 kVA) have been " + "found: 'SE_Minera_A0Ak_50kVA', 'SE_Minera_B0Bk_50kVA', 'SE_Minera_C0Bk_50kVA', " + "'SE_Minera_Standard_50kVA'." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND -def test_print_catalogue(): - # Print the entire catalogue - with console.capture() as capture: - TransformerParameters.print_catalogue() - assert len(capture.get().split("\n")) == 136 +def test_get_catalogue(): + # Get the entire catalogue + catalogue = TransformerParameters.get_catalogue() + assert isinstance(catalogue, pd.DataFrame) + assert catalogue.shape == (130, 7) # Filter on a single attribute - for field_name, value, expected_lines in ( - ("id", "SE_Minera_A0Ak_50kVA", 7), - ("manufacturer", "SE", 122), - ("range", r"min.*", 62), - ("efficiency", "c0", 35), - ("type", "dy", 132), - ("sn", Q_(160, "kVA"), 16), - ("uhv", Q_(20, "kV"), 136), - ("ulv", 400, 136), + for field_name, value, expected_size in ( + ("id", "SE_Minera_A0Ak_50kVA", 1), + ("manufacturer", "SE", 116), + ("range", r"min.*", 56), + ("efficiency", "c0", 29), + ("type", "dy", 126), + ("sn", Q_(160, "kVA"), 10), + ("uhv", Q_(20, "kV"), 130), + ("ulv", 400, 130), ): - with console.capture() as capture: - TransformerParameters.print_catalogue(**{field_name: value}) - assert len(capture.get().split("\n")) == expected_lines + filtered_catalogue = TransformerParameters.get_catalogue(**{field_name: value}) + assert filtered_catalogue.shape == (expected_size, 7) # Filter on two attributes - for field_name, value, expected_lines in ( - ("id", "SE_Minera_A0Ak_50kVA", 7), - ("range", "minera", 62), - ("efficiency", "c0", 35), - ("type", r"^d.*11$", 118), - ("sn", Q_(160, "kVA"), 15), - ("uhv", Q_(20, "kV"), 122), - ("ulv", 400, 122), + for field_name, value, expected_size in ( + ("id", "SE_Minera_A0Ak_50kVA", 1), + ("range", "minera", 56), + ("efficiency", "c0", 29), + ("type", r"^d.*11$", 112), + ("sn", Q_(160, "kVA"), 9), + ("uhv", Q_(20, "kV"), 116), + ("ulv", 400, 116), ): - with console.capture() as capture: - TransformerParameters.print_catalogue(**{field_name: value}, manufacturer="se") - assert len(capture.get().split("\n")) == expected_lines + filtered_catalogue = TransformerParameters.get_catalogue(**{field_name: value}, manufacturer="se") + assert filtered_catalogue.shape == (expected_size, 7) # Filter on three attributes - for field_name, value, expected_lines in ( - ("id", "se_VEGETA_C0BK_3150kva", 7), - ("efficiency", r"c0[abc]k", 21), - ("type", "dyn", 36), - ("sn", Q_(160, "kVA"), 8), - ("uhv", Q_(20, "kV"), 36), - ("ulv", 400, 36), + for field_name, value, expected_size in ( + ("id", "se_VEGETA_C0BK_3150kva", 1), + ("efficiency", r"c0[abc]k", 15), + ("type", "dyn", 30), + ("sn", Q_(160, "kVA"), 2), + ("uhv", Q_(20, "kV"), 30), + ("ulv", 400, 30), ): - with console.capture() as capture: - TransformerParameters.print_catalogue(**{field_name: value}, manufacturer="se", range=r"^vegeta$") - assert len(capture.get().split("\n")) == expected_lines + filtered_catalogue = TransformerParameters.get_catalogue( + **{field_name: value}, manufacturer="se", range=r"^vegeta$" + ) + assert filtered_catalogue.shape == (expected_size, 7) # No results - with console.capture() as capture: - TransformerParameters.print_catalogue(ulv=250) - assert len(capture.get().split("\n")) == 2 + empty_catalogue = TransformerParameters.get_catalogue(ulv=250) + assert empty_catalogue.shape == (0, 7) def test_max_power(): diff --git a/roseau/load_flow/models/transformers/parameters.py b/roseau/load_flow/models/transformers/parameters.py index aa596ee5..9ee332e2 100644 --- a/roseau/load_flow/models/transformers/parameters.py +++ b/roseau/load_flow/models/transformers/parameters.py @@ -1,21 +1,19 @@ +import json import logging import re -import textwrap from importlib import resources -from itertools import cycle from pathlib import Path from typing import NoReturn import numpy as np import pandas as pd import regex -from rich.table import Table from typing_extensions import Self from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps -from roseau.load_flow.utils import CatalogueMixin, Identifiable, JsonMixin, console, palette +from roseau.load_flow.utils import CatalogueMixin, Identifiable, JsonMixin logger = logging.getLogger(__name__) @@ -312,7 +310,71 @@ def catalogue_path(cls) -> Path: @classmethod def catalogue_data(cls) -> pd.DataFrame: - return pd.read_csv(cls.catalogue_path() / "Catalogue.csv") + file = cls.catalogue_path() / "Catalogue.csv" + return pd.read_csv(file, parse_dates=False) + + @classmethod + def _get_catalogue( + cls, + id: str | re.Pattern[str] | None, + manufacturer: str | re.Pattern[str] | None, + range: str | re.Pattern[str] | None, + efficiency: str | re.Pattern[str] | None, + type: str | re.Pattern[str] | None, + sn: float | None, + uhv: float | None, + ulv: float | None, + raise_if_not_found: bool, + ) -> tuple[pd.DataFrame, str]: + # Get the catalogue data + catalogue_data = cls.catalogue_data() + + # Filter on string/regular expressions + query_msg_list = [] + for value, column_name, display_name, display_name_plural in ( + (id, "id", "id", "ids"), + (manufacturer, "manufacturer", "manufacturer", "manufacturers"), + (range, "range", "range", "ranges"), + (efficiency, "efficiency", "efficiency", "efficiencies"), + (type, "type", "type", "types"), + ): + if pd.isna(value): + continue + + mask = cls._filter_catalogue_str(value=value, strings=catalogue_data[column_name]) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=repr(value), + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name], + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r}") + + # Filter on float + for value, column_name, display_name, display_name_plural, display_unit in ( + (sn, "sn", "nominal power", "nominal powers", "kVA"), + (uhv, "uhv", "primary side voltage", "primary side voltages", "kV"), + (ulv, "ulv", "secondary side voltage", "secondary side voltages", "kV"), + ): + if pd.isna(value): + continue + + mask = np.isclose(catalogue_data[column_name], value) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=f"{value / 1000:.1f} {display_unit}", + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name].apply(lambda x: f"{x/1000:.1f} {display_unit}"), # noqa: B023 + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value/1000:.1f} {display_unit}") + + return catalogue_data, ", ".join(query_msg_list) @classmethod @ureg_wraps(None, (None, None, None, None, None, None, "VA", "V", "V")) @@ -323,9 +385,9 @@ def from_catalogue( range: str | re.Pattern[str] | None = None, efficiency: str | re.Pattern[str] | None = None, type: str | re.Pattern[str] | None = None, - sn: float | None = None, - uhv: float | None = None, - ulv: float | None = None, + sn: float | Q_[float] | None = None, + uhv: float | Q_[float] | None = None, + ulv: float | Q_[float] | None = None, ) -> Self: """Build a transformer parameters from one in the catalogue. @@ -359,120 +421,57 @@ def from_catalogue( raised. """ # Get the catalogue data - catalogue_data = cls.catalogue_data() - - # Filter on string/regular expressions - query_msg_list = [] - for value, column_name, display_name, display_name_plural in ( - (id, "id", "id", "ids"), - (manufacturer, "manufacturer", "manufacturer", "manufacturers"), - (range, "range", "range", "ranges"), - (efficiency, "efficiency", "efficiency", "efficiencies"), - (type, "type", "type", "types"), - ): - if pd.isna(value): - continue - - mask = cls._filter_catalogue_str(value=value, catalogue_data=catalogue_data, column_name=column_name) - if mask.sum() == 0: - available_values = catalogue_data[column_name].unique().tolist() - msg_part = textwrap.shorten(", ".join(repr(x) for x in available_values), width=500) - if query_msg_list: - query_msg_part = ", ".join(query_msg_list) - msg = ( - f"No {display_name} matching the name {value!r} has been found for the query {query_msg_part}. " - f"Available {display_name_plural} are {msg_part}." - ) - else: - msg = ( - f"No {display_name} matching the name {value!r} has been found. " - f"Available {display_name_plural} are {msg_part}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - catalogue_data = catalogue_data.loc[mask, :] - query_msg_list.append(f"{display_name}={value!r}") - - # Filter on float - for value, column_name, display_name, display_name_plural, display_unit in ( - (sn, "sn", "nominal power", "nominal powers", "kVA"), - (uhv, "uhv", "primary side voltage", "primary side voltages", "kV"), - (ulv, "ulv", "secondary side voltage", "secondary side voltages", "kV"), - ): - if pd.isna(value): - continue - - mask = cls._filter_catalogue_float(value=value, catalogue_data=catalogue_data, column_name=column_name) - if mask.sum() == 0: - available_values = catalogue_data[column_name].unique().tolist() - msg_part = textwrap.shorten( - ", ".join(f"{x/1000:.1f} {display_unit}" for x in available_values), width=500 - ) - if query_msg_list: - query_msg_part = ", ".join(query_msg_list) - msg = ( - f"No {display_name} matching {value/1000:.1f} {display_unit} has been found for the query" - f" {query_msg_part}. Available {display_name_plural} are {msg_part}." - ) - else: - msg = ( - f"No {display_name} matching {value/1000:.1f} {display_unit} has been found. " - f"Available {display_name_plural} are {msg_part}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - catalogue_data = catalogue_data.loc[mask, :] - query_msg_list.append(f"{display_name}={value/1000:.1f} {display_unit}") + catalogue_data, query_info = cls._get_catalogue( + id=id, + manufacturer=manufacturer, + range=range, + efficiency=efficiency, + type=type, + sn=sn, + uhv=uhv, + ulv=ulv, + raise_if_not_found=True, + ) - # Final check - if len(catalogue_data) == 0: # pragma: no cover - # This option should never happen as an error is raised when a filter is empty - query_msg_part = ", ".join(query_msg_list) - msg = ( - f"No transformers matching the query ({query_msg_part!r}) have been found. Please look at the " - f"catalogue using the `print_catalogue` class method." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - elif len(catalogue_data) > 1: - query_msg_part = ", ".join(query_msg_list) - msg = ( - f"Several transformers matching the query ({query_msg_part!r}) have been found. Please look at the " - f"catalogue using the `print_catalogue` class method." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND) + cls._assert_one_found( + found_data=catalogue_data["id"].tolist(), display_name="transformers", query_info=query_info + ) # A single one has been chosen idx = catalogue_data.index[0] - manufacturer = catalogue_data.at[idx, "manufacturer"] - range = catalogue_data.at[idx, "range"] - efficiency = catalogue_data.at[idx, "efficiency"] + manufacturer = str(catalogue_data.at[idx, "manufacturer"]) + range = str(catalogue_data.at[idx, "range"]) + efficiency = str(catalogue_data.at[idx, "efficiency"]) nominal_power = int(catalogue_data.at[idx, "sn"] / 1000) # Get the data from the Json file path = cls.catalogue_path() / manufacturer / range / efficiency / f"{nominal_power}.json" - if not path.exists(): # pragma: no cover + try: + json_dict = json.loads(path.read_text()) + except FileNotFoundError: msg = f"The file {path} has not been found while it should exist. Please post an issue on GitHub." logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_MISSING) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_MISSING) from None - return cls.from_json(path=path) + return cls.from_dict(json_dict) @classmethod @ureg_wraps(None, (None, None, None, None, None, None, "VA", "V", "V")) - def print_catalogue( + def get_catalogue( cls, id: str | re.Pattern[str] | None = None, manufacturer: str | re.Pattern[str] | None = None, range: str | re.Pattern[str] | None = None, efficiency: str | re.Pattern[str] | None = None, type: str | re.Pattern[str] | None = None, - sn: float | None = None, - uhv: float | None = None, - ulv: float | None = None, - ) -> None: - """Print the catalogue of available transformers. + sn: float | Q_[float] | None = None, + uhv: float | Q_[float] | None = None, + ulv: float | Q_[float] | None = None, + ) -> pd.DataFrame: + """Get the catalogue of available transformers. + + You can use the parameters below to filter the catalogue. If you do not specify any + parameter, all the catalogue will be returned. Args: id: @@ -498,122 +497,45 @@ def print_catalogue( ulv: An optional secondary side voltage to filter the output. - """ - # Get the catalogue data - catalogue_data = cls.catalogue_data() - - # Start creating a table to display the results - table = Table(title="Available Transformer Parameters") - table.add_column("Id", overflow="fold") - table.add_column("Manufacturer", overflow="fold") - table.add_column("Product range", overflow="fold") - table.add_column("Efficiency", overflow="fold") - table.add_column("Type", overflow="fold") - table.add_column("Nominal power (kVA)", justify="right", overflow="fold") - table.add_column("High voltage (kV)", justify="right", overflow="fold") - table.add_column("Low voltage (kV)", justify="right", overflow="fold") - empty_table = True - - # Match on the manufacturer, range, efficiency and type - catalogue_mask = pd.Series(True, index=catalogue_data.index) - query_msg_list = [] - for value, column_name in ( - (id, "id"), - (manufacturer, "manufacturer"), - (range, "range"), - (efficiency, "efficiency"), - (type, "type"), - ): - if pd.isna(value): - continue - catalogue_mask &= cls._filter_catalogue_str( - value=value, catalogue_data=catalogue_data, column_name=column_name - ) - query_msg_list.append(f"{column_name}={value!r}") - - # Mask on nominal power, primary and secondary voltages - for value, column_name, display_unit in ((uhv, "uhv", "kV"), (ulv, "ulv", "kV"), (sn, "sn", "kVA")): - if pd.isna(value): - continue - catalogue_mask &= cls._filter_catalogue_float( - value=value, catalogue_data=catalogue_data, column_name=column_name - ) - query_msg_list.append(f"{column_name}={value/1000:.1f} {display_unit}") - - # Iterate over the transformers - selected_index = catalogue_mask[catalogue_mask].index - cycler = cycle(palette) - for idx in selected_index: - empty_table = False - table.add_row( - catalogue_data.at[idx, "id"], - catalogue_data.at[idx, "manufacturer"], - catalogue_data.at[idx, "range"], - catalogue_data.at[idx, "efficiency"], - catalogue_data.at[idx, "type"], - f"{catalogue_data.at[idx, 'sn']/1000:.1f}", # VA to kVA - f"{catalogue_data.at[idx, 'uhv']/1000:.1f}", # V to kV - f"{catalogue_data.at[idx, 'ulv']/1000:.1f}", # V to kV - style=next(cycler), - ) - - # Handle the case of an empty table - if empty_table: - query_msg_part = ", ".join(query_msg_list) - msg = f"No transformers can be found in the catalogue matching your query: {query_msg_part}." - console.print(msg) - else: - console.print(table) - - @staticmethod - def _filter_catalogue_str( - value: str | re.Pattern[str], catalogue_data: pd.DataFrame, column_name: str - ) -> pd.Series: - """Filter the catalogue using a string/regexp value. - - Args: - value: - The string or regular expression to use as a filter. - - catalogue_data: - The catalogue data to use. - - column_name: - The name of the column to use for the filter. Returns: - The mask of matching results. + The catalogue data as a dataframe. """ - if isinstance(value, re.Pattern): - return catalogue_data[column_name].str.match(value) - else: - try: - pattern = re.compile(pattern=value, flags=re.IGNORECASE) - return catalogue_data[column_name].str.match(pattern) - except re.error: - return catalogue_data[column_name].str.lower() == value.lower() - - @staticmethod - def _filter_catalogue_float(value: float, catalogue_data: pd.DataFrame, column_name: str) -> pd.Series: - """Filter the catalogue using a float/int value. - - Args: - value: - The float or integer to use as a filter. - - catalogue_data: - The catalogue data to use. - - column_name: - The name of the column to use for the filter. - - Returns: - The mask of matching results. - """ - if isinstance(value, int): - return catalogue_data[column_name] == value - else: - return np.isclose(catalogue_data[column_name], value) + catalogue_data, _ = cls._get_catalogue( + id=id, + manufacturer=manufacturer, + range=range, + efficiency=efficiency, + type=type, + sn=sn, + uhv=uhv, + ulv=ulv, + raise_if_not_found=False, + ) + catalogue_data["sn"] /= 1000 # kVA + catalogue_data["uhv"] /= 1000 # kV + catalogue_data["ulv"] /= 1000 # kV + return ( + catalogue_data.drop(columns=["i0", "p0", "psc", "vsc"]) + .rename( + columns={ + "id": "Id", + "manufacturer": "Manufacturer", + "range": "Product range", + "efficiency": "Efficiency", + "type": "Type", + "sn": "Nominal power (kVA)", + "uhv": "High voltage (kV)", + "ulv": "Low voltage (kV)", + # # If we ever want to display these columns + # "i0": "No-load current (%)", + # "p0": "No-load losses (W)", + # "psc": "Load Losses at 75°C (W)", + # "vsc": "Impedance voltage (%)", + } + ) + .set_index("Id") + ) # # Utils diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index f4b2526f..53464916 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -4,12 +4,11 @@ import json import logging import re -import textwrap import time import warnings -from collections.abc import Mapping, Sized +from collections.abc import Iterable, Mapping, Sized from importlib import resources -from itertools import chain, cycle +from itertools import chain from pathlib import Path from typing import TYPE_CHECKING, NoReturn, TypeVar @@ -17,7 +16,6 @@ import numpy as np import pandas as pd from pyproj import CRS -from rich.table import Table from typing_extensions import Self from roseau.load_flow._solvers import AbstractSolver @@ -37,7 +35,7 @@ VoltageSource, ) from roseau.load_flow.typing import Id, JsonDict, MapOrSeq, Solver, StrPath -from roseau.load_flow.utils import CatalogueMixin, JsonMixin, _optional_deps, console, palette +from roseau.load_flow.utils import CatalogueMixin, JsonMixin, _optional_deps from roseau.load_flow.utils.types import _DTYPES, VoltagePhaseDtype from roseau.load_flow_engine.cy_engine import CyElectricalNetwork @@ -1303,7 +1301,7 @@ def _propagate_potentials(self) -> None: elements.append((e, potentials)) @staticmethod - def _check_ref(elements: list[Element]) -> None: + def _check_ref(elements: Iterable[Element]) -> None: """Check the number of potential references to avoid having a singular jacobian matrix.""" visited_elements: set[Element] = set() for initial_element in elements: @@ -1461,6 +1459,68 @@ def catalogue_path(cls) -> Path: def catalogue_data(cls) -> JsonDict: return json.loads((cls.catalogue_path() / "Catalogue.json").read_text()) + @classmethod + def _get_catalogue( + cls, name: str | re.Pattern[str] | None, load_point_name: str | re.Pattern[str] | None, raise_if_not_found: bool + ) -> tuple[pd.DataFrame, str]: + # Get the catalogue data + catalogue_data = cls.catalogue_data() + + catalogue_dict = { + "name": [], + "nb_buses": [], + "nb_branches": [], + "nb_loads": [], + "nb_sources": [], + "nb_grounds": [], + "nb_potential_refs": [], + "load_points": [], + } + query_msg_list = [] + + # Match on the name + available_names = list(catalogue_data) + match_names_list = available_names + if name is not None: + match_names_list = cls._filter_catalogue_str(name, strings=available_names) + if isinstance(name, re.Pattern): + name = name.pattern + query_msg_list.append(f"{name=!r}") + if raise_if_not_found: + cls._assert_one_found(found_data=match_names_list, display_name="networks", query_info=f"{name=!r}") + + if load_point_name is not None: + load_point_name_str = load_point_name if isinstance(load_point_name, str) else load_point_name.pattern + query_msg_list.append(f"load_point_name={load_point_name_str!r}") + + for name in match_names_list: + network_data = catalogue_data[name] + + # Match on the load point + available_load_points: list[str] = network_data["load_points"] + match_load_point_names_list = available_load_points + if load_point_name is not None: + match_load_point_names_list = cls._filter_catalogue_str(load_point_name, strings=available_load_points) + if raise_if_not_found: + cls._assert_one_found( + found_data=match_load_point_names_list, + display_name=f"load points for network {name!r}", + query_info=query_msg_list[-1], + ) + elif not match_load_point_names_list: + continue + + catalogue_dict["name"].append(name) + catalogue_dict["nb_buses"].append(network_data["nb_buses"]) + catalogue_dict["nb_branches"].append(network_data["nb_branches"]) + catalogue_dict["nb_loads"].append(network_data["nb_loads"]) + catalogue_dict["nb_sources"].append(network_data["nb_sources"]) + catalogue_dict["nb_grounds"].append(network_data["nb_grounds"]) + catalogue_dict["nb_potential_refs"].append(network_data["nb_potential_refs"]) + catalogue_dict["load_points"].append(match_load_point_names_list) + + return pd.DataFrame(catalogue_dict), ", ".join(query_msg_list) + @classmethod def from_catalogue(cls, name: str | re.Pattern[str], load_point_name: str | re.Pattern[str]) -> Self: """Build a network from one in the catalogue. @@ -1477,188 +1537,62 @@ def from_catalogue(cls, name: str | re.Pattern[str], load_point_name: str | re.P The selected network """ # Get the catalogue data - catalogue_data = cls.catalogue_data() + catalogue_data, _ = cls._get_catalogue( + name=name, + load_point_name=load_point_name, + raise_if_not_found=True, + ) - # Match on the name - if isinstance(name, re.Pattern): - name_pattern = name - name = name.pattern - match_names_list = [k for k in catalogue_data if name_pattern.match(k)] - else: - try: - name_pattern = re.compile(pattern=name, flags=re.IGNORECASE) - match_names_list = [k for k in catalogue_data if name_pattern.match(k)] - except re.error: - name_pattern = name.lower() - match_names_list = [k for k in catalogue_data if k.lower() == name_pattern] - if not match_names_list: - msg = ( - f"No network matching the name {name!r} has been found. " - f"Please look at the catalogue using the `print_catalogue` class method." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - elif len(match_names_list) > 1: - msg_part = textwrap.shorten(", ".join(repr(x) for x in sorted(match_names_list)), width=500) - msg = f"Several networks matching the name {name!r} have been found: {msg_part}." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND) - name = match_names_list[0] - - # Match on the load point - c_data = catalogue_data[name] - available_load_points = c_data["load_points"] - if isinstance(load_point_name, re.Pattern): - load_point_name_pattern = load_point_name - load_point_name = load_point_name.pattern - match_load_point_names_list = [k for k in available_load_points if load_point_name_pattern.match(k)] - else: - try: - load_point_name_pattern = re.compile(pattern=load_point_name, flags=re.IGNORECASE) - match_load_point_names_list = [k for k in available_load_points if load_point_name_pattern.match(k)] - except re.error: - load_point_name_pattern = load_point_name.lower() - match_load_point_names_list = [k for k in available_load_points if k.lower() == load_point_name_pattern] - if not match_load_point_names_list: - msg_part = textwrap.shorten(", ".join(repr(x) for x in sorted(available_load_points)), width=500) - msg = ( - f"No load point matching the name {load_point_name!r} has been found for the network {name!r}. " - f"Available load points are {msg_part}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - elif len(match_load_point_names_list) > 1: - msg_part = textwrap.shorten(", ".join(repr(x) for x in sorted(match_load_point_names_list)), width=500) - msg = ( - f"Several load points matching the name {load_point_name!r} have been found for the network " - f"{name!r}: {msg_part}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND) - load_point_name = match_load_point_names_list[0] + name = catalogue_data["name"].item() + load_point_name = catalogue_data["load_points"].item()[0] # Get the data from the Json file path = cls.catalogue_path() / f"{name}_{load_point_name}.json" - if not path.exists(): # pragma: no cover + try: + json_dict = json.loads(path.read_text()) + except FileNotFoundError: msg = f"The file {path} has not been found while it should exist. Please post an issue on GitHub." logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_MISSING) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_MISSING) from None - return cls.from_json(path=path) + return cls.from_dict(json_dict) @classmethod - def print_catalogue( - cls, - name: str | re.Pattern[str] | None = None, - load_point_name: str | re.Pattern[str] | None = None, - ) -> None: - """Print the catalogue of available networks. + def get_catalogue( + cls, name: str | re.Pattern[str] | None = None, load_point_name: str | re.Pattern[str] | None = None + ) -> pd.DataFrame: + """Read a network dictionary from the catalogue. Args: name: - The name of the networks to display. It can be a regular expression. For instance, `name="lv"` will - match all the network name starting with "lv" (ignoring case). + The name of the network to get from the catalogue. It can be a regular expression. load_point_name: - Only networks having a load point matching this string or regular expression will be displayed. - """ - # Get the catalogue data - catalogue_data = cls.catalogue_data() - - # Start creating a table to display the results - table = Table(title="Available Networks") - table.add_column("Name", overflow="fold") - table.add_column("Nb buses", justify="right", overflow="fold") - table.add_column("Nb branches", justify="right", overflow="fold") - table.add_column("Nb loads", justify="right", overflow="fold") - table.add_column("Nb sources", justify="right", overflow="fold") - table.add_column("Nb grounds", justify="right", overflow="fold") - table.add_column("Nb potential refs", justify="right", overflow="fold") - table.add_column("Available load points", overflow="fold") - empty_table = True - - # Match on the name - match_names_list = cls._filter_name(name=name, catalogue_data=catalogue_data) - - # Match on load point name - if load_point_name is None: - load_point_name_pattern = None - - def match_load_point_function(x: str) -> bool: - return True - - elif isinstance(load_point_name, re.Pattern): - load_point_name_pattern = load_point_name - load_point_name = load_point_name.pattern - match_load_point_function = load_point_name_pattern.match - else: - try: - load_point_name_pattern = re.compile(pattern=load_point_name, flags=re.IGNORECASE) - match_load_point_function = load_point_name_pattern.match - except re.error: - load_point_name_pattern = name.lower() - - def match_load_point_function(x: str) -> bool: - nonlocal load_point_name_pattern - return x.lower() == load_point_name_pattern - - # Iterate over the networks - cycler = cycle(palette) - for c_name in match_names_list: - c_data = catalogue_data[c_name] - available_load_points = c_data["load_points"] - if any(match_load_point_function(x) for x in available_load_points): - empty_table = False - table.add_row( - c_name, - str(c_data["nb_buses"]), - str(c_data["nb_branches"]), - str(c_data["nb_loads"]), - str(c_data["nb_sources"]), - str(c_data["nb_grounds"]), - str(c_data["nb_potential_refs"]), - ", ".join(repr(x) for x in sorted(c_data["load_points"])), - style=next(cycler), - ) - - # Handle the case of an empty table - if empty_table: - msg = "No networks can be found in the catalogue" - if name is not None and load_point_name is not None: - msg += f" with the name {name!r} and having a load point named {load_point_name!r}" - elif name is not None: - msg += f" with the name {name!r}" - elif load_point_name is not None: - msg += f" having a load point named {load_point_name!r}" - msg += "!" - console.print(msg) - else: - console.print(table) - - @staticmethod - def _filter_name(name: str | re.Pattern[str] | None, catalogue_data: JsonDict) -> list[str]: - """Filter the catalogue using the network name. - - Args: - name: - The optional name to use as a filter. - - catalogue_data: - The catalogue of available networks. It avoids an additional read. + The name of the load point to get. For each network, several load points may be available. It can be + a regular expression. Returns: - The list of network names matching the provided one. + The dictionary containing the network data. """ - if name is None: - match_names_list = list(catalogue_data) - elif isinstance(name, re.Pattern): - match_names_list = [k for k in catalogue_data if name.match(k)] - else: - try: - name_pattern = re.compile(pattern=name, flags=re.IGNORECASE) - match_names_list = [k for k in catalogue_data if name_pattern.match(k)] - except re.error: - name_pattern = name.lower() - match_names_list = [k for k in catalogue_data if k.lower() == name_pattern] - - return match_names_list + + catalogue_data, _ = cls._get_catalogue( + name=name, + load_point_name=load_point_name, + raise_if_not_found=False, + ) + return ( + catalogue_data.reset_index(drop=True) + .rename( + columns={ + "name": "Name", + "nb_buses": "Nb buses", + "nb_branches": "Nb branches", + "nb_loads": "Nb loads", + "nb_sources": "Nb sources", + "nb_grounds": "Nb grounds", + "nb_potential_refs": "Nb potential refs", + "load_points": "Available load points", + } + ) + .set_index("Name") + ) diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index cde07bc2..4ca836ce 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -27,7 +27,7 @@ ) from roseau.load_flow.network import ElectricalNetwork from roseau.load_flow.units import Q_ -from roseau.load_flow.utils import BranchTypeDtype, PhaseDtype, VoltagePhaseDtype, console +from roseau.load_flow.utils import BranchTypeDtype, PhaseDtype, VoltagePhaseDtype @pytest.fixture() @@ -202,8 +202,8 @@ def test_connect_and_disconnect(): assert load.bus is None with pytest.raises(RoseauLoadFlowException) as e: load.to_dict() - assert e.value.args[0] == "The load 'power load' is disconnected and cannot be used anymore." - assert e.value.args[1] == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT + assert e.value.msg == "The load 'power load' is disconnected and cannot be used anymore." + assert e.value.code == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT new_load = PowerLoad(id="power load", phases="abcn", bus=load_bus, powers=[100 + 0j, 100 + 0j, 100 + 0j]) assert new_load.network == en @@ -214,8 +214,8 @@ def test_connect_and_disconnect(): assert vs.bus is None with pytest.raises(RoseauLoadFlowException) as e: vs.to_dict() - assert e.value.args[0] == "The voltage source 'vs' is disconnected and cannot be used anymore." - assert e.value.args[1] == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT + assert e.value.msg == "The voltage source 'vs' is disconnected and cannot be used anymore." + assert e.value.code == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT # Bad key with pytest.raises(RoseauLoadFlowException) as e: @@ -261,7 +261,7 @@ def test_recursive_connect_disconnect(): new_load2 = PowerLoad(id="new_load2", bus=new_bus2, phases="abcn", powers=Q_([100, 0, 0], "VA")) new_bus = Bus(id="new_bus", phases="abcn") new_load = PowerLoad(id="new_load", bus=new_bus, phases="abcn", powers=Q_([100, 0, 0], "VA")) - lp = LineParameters("S_AL_240_without_shunt", z_line=Q_(0.1 * np.eye(4), "ohm/km"), y_shunt=None) + lp = LineParameters("U_AL_240_without_shunt", z_line=Q_(0.1 * np.eye(4), "ohm/km"), y_shunt=None) new_line2 = Line( id="new_line2", bus1=new_bus2, @@ -366,7 +366,7 @@ def test_recursive_connect_disconnect_ground(): assert new_load2.id not in en.loads lp = LineParameters( - "S_AL_240_with_shunt", z_line=Q_(0.1 * np.eye(4), "ohm/km"), y_shunt=Q_(0.1 * np.eye(4), "S/km") + "U_AL_240_with_shunt", z_line=Q_(0.1 * np.eye(4), "ohm/km"), y_shunt=Q_(0.1 * np.eye(4), "S/km") ) new_line2 = Line( id="new_line2", @@ -962,8 +962,8 @@ def test_network_elements(small_network: ElectricalNetwork): # Connect the two networks with pytest.raises(RoseauLoadFlowException) as e: Switch("switch2", bus1=bus2, bus2=bus_vs) - assert e.value.args[0] == "The Bus 'bus_vs' is already assigned to another network." - assert e.value.args[1] == RoseauLoadFlowExceptionCode.SEVERAL_NETWORKS + assert e.value.msg == "The Bus 'bus_vs' is already assigned to another network." + assert e.value.code == RoseauLoadFlowExceptionCode.SEVERAL_NETWORKS # Every object have their good network after this failure for element in it.chain( @@ -1003,34 +1003,34 @@ def test_network_results_warning(small_network: ElectricalNetwork, recwarn): # for bus in small_network.buses.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = bus.res_potentials - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN with pytest.raises(RoseauLoadFlowException) as e: _ = bus.res_voltages - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for branch in small_network.branches.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = branch.res_currents - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for load in small_network.loads.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = load.res_currents - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN if load.is_flexible and isinstance(load, PowerLoad): with pytest.raises(RoseauLoadFlowException) as e: _ = load.res_flexible_powers - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for source in small_network.sources.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = source.res_currents - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for ground in small_network.grounds.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = ground.res_potential - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for p_ref in small_network.potential_refs.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = p_ref.res_current - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN # Solve a load flow small_network.solve_load_flow() @@ -1739,89 +1739,78 @@ def test_from_catalogue(): # Unknown network name with pytest.raises(RoseauLoadFlowException) as e: ElectricalNetwork.from_catalogue(name="unknown", load_point_name="winter") - assert ( - e.value.args[0] - == "No network matching the name 'unknown' has been found. Please look at the catalogue using the " - "`print_catalogue` class method." + assert e.value.msg == ( + "No networks matching the query (name='unknown') have been found. Please look at the " + "catalogue using the `get_catalogue` class method." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Unknown load point name with pytest.raises(RoseauLoadFlowException) as e: ElectricalNetwork.from_catalogue(name="MVFeeder004", load_point_name="unknown") - assert ( - e.value.args[0] - == "No load point matching the name 'unknown' has been found for the network 'MVFeeder004'. Available " - "load points are 'Summer', 'Winter'." + assert e.value.msg == ( + "No load points for network 'MVFeeder004' matching the query (load_point_name='unknown') have " + "been found. Please look at the catalogue using the `get_catalogue` class method." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Several network name matched with pytest.raises(RoseauLoadFlowException) as e: ElectricalNetwork.from_catalogue(name="MVFeeder", load_point_name="winter") - assert e.value.args[0] == ( - "Several networks matching the name 'MVFeeder' have been found: 'MVFeeder004', " - "'MVFeeder011', 'MVFeeder015', 'MVFeeder032', 'MVFeeder041', 'MVFeeder063', 'MVFeeder078', 'MVFeeder115', " - "'MVFeeder128', 'MVFeeder151', 'MVFeeder159', 'MVFeeder176', 'MVFeeder210', 'MVFeeder217', 'MVFeeder232'," - " 'MVFeeder251', 'MVFeeder290', 'MVFeeder312', 'MVFeeder320', 'MVFeeder339'." + assert e.value.msg == ( + "Several networks matching the query (name='MVFeeder') have been found: 'MVFeeder004', " + "'MVFeeder011', 'MVFeeder015', 'MVFeeder032', 'MVFeeder041', 'MVFeeder063', 'MVFeeder078', " + "'MVFeeder115', 'MVFeeder128', 'MVFeeder151', 'MVFeeder159', 'MVFeeder176', 'MVFeeder210', " + "'MVFeeder217', 'MVFeeder232', 'MVFeeder251', 'MVFeeder290', 'MVFeeder312', 'MVFeeder320', " + "'MVFeeder339'." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND # Several load point name matched with pytest.raises(RoseauLoadFlowException) as e: ElectricalNetwork.from_catalogue(name="MVFeeder004", load_point_name=r".*") - assert e.value.args[0] == ( - "Several load points matching the name '.*' have been found for the network 'MVFeeder004': 'Summer', 'Winter'." + assert e.value.msg == ( + "Several load points for network 'MVFeeder004' matching the query (load_point_name='.*') have " + "been found: 'Summer', 'Winter'." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND # Both known ElectricalNetwork.from_catalogue(name="MVFeeder004", load_point_name="winter") -def test_print_catalogue(): - # Print the entire catalogue - with console.capture() as capture: - ElectricalNetwork.print_catalogue() - assert len(capture.get().split("\n")) == 46 +def test_get_catalogue(): + # Get the entire catalogue + catalogue = ElectricalNetwork.get_catalogue() + assert catalogue.shape == (40, 7) # Filter on the network name - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name="MV") - assert len(capture.get().split("\n")) == 26 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name=re.compile(r"^MV")) - assert len(capture.get().split("\n")) == 26 + catalogue = ElectricalNetwork.get_catalogue(name="MV") + assert catalogue.shape == (20, 7) + catalogue = ElectricalNetwork.get_catalogue(name=re.compile(r"^MV")) + assert catalogue.shape == (20, 7) # Filter on the load point name - with console.capture() as capture: - ElectricalNetwork.print_catalogue(load_point_name="winter") - assert len(capture.get().split("\n")) == 46 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(load_point_name=re.compile(r"^Winter")) - assert len(capture.get().split("\n")) == 46 + catalogue = ElectricalNetwork.get_catalogue(load_point_name="winter") + assert catalogue.shape == (40, 7) + catalogue = ElectricalNetwork.get_catalogue(load_point_name=re.compile(r"^Winter")) + assert catalogue.shape == (40, 7) # Filter on both - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name="MV", load_point_name="winter") - assert len(capture.get().split("\n")) == 26 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name="MV", load_point_name=re.compile(r"^Winter")) - assert len(capture.get().split("\n")) == 26 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name=re.compile(r"^MV"), load_point_name="winter") - assert len(capture.get().split("\n")) == 26 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name=re.compile(r"^MV"), load_point_name=re.compile(r"^Winter")) - assert len(capture.get().split("\n")) == 26 + catalogue = ElectricalNetwork.get_catalogue(name="MV", load_point_name="winter") + assert catalogue.shape == (20, 7) + catalogue = ElectricalNetwork.get_catalogue(name="MV", load_point_name=re.compile(r"^Winter")) + assert catalogue.shape == (20, 7) + catalogue = ElectricalNetwork.get_catalogue(name=re.compile(r"^MV"), load_point_name="winter") + assert catalogue.shape == (20, 7) + catalogue = ElectricalNetwork.get_catalogue(name=re.compile(r"^MV"), load_point_name=re.compile(r"^Winter")) + assert catalogue.shape == (20, 7) # Regexp error - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name=r"^MV[0-") - assert len(capture.get().split("\n")) == 2 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(load_point_name=r"^winter[0-]") - assert len(capture.get().split("\n")) == 2 + catalogue = ElectricalNetwork.get_catalogue(name=r"^MV[0-") + assert catalogue.empty + catalogue = ElectricalNetwork.get_catalogue(load_point_name=r"^winter[0-]") + assert catalogue.empty def test_to_graph(small_network: ElectricalNetwork): diff --git a/roseau/load_flow/tests/test_exceptions.py b/roseau/load_flow/tests/test_exceptions.py index 4b54635b..fcc2e3db 100644 --- a/roseau/load_flow/tests/test_exceptions.py +++ b/roseau/load_flow/tests/test_exceptions.py @@ -3,17 +3,13 @@ def test_exceptions(): for x in RoseauLoadFlowExceptionCode: - # String starts with the package name - assert str(x).startswith("roseau.load_flow.") - - # String equality - assert str(x) == x - - # No equality without the prefix - assert str(x).removeprefix("roseau.load_flow.") != x - # Case-insensitive assert str(x).upper() == x + assert str(x).lower() == x + # Case-insensitive constructor (with or without spaces or dashes) + assert RoseauLoadFlowExceptionCode("BaD_bus_ID") == RoseauLoadFlowExceptionCode.BAD_BUS_ID + assert RoseauLoadFlowExceptionCode("bad bus id") == RoseauLoadFlowExceptionCode.BAD_BUS_ID + assert RoseauLoadFlowExceptionCode("BAD-BUS-ID") == RoseauLoadFlowExceptionCode.BAD_BUS_ID r = RoseauLoadFlowException(msg="toto", code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS) assert r.msg == "toto" diff --git a/roseau/load_flow/utils/__init__.py b/roseau/load_flow/utils/__init__.py index e774d8d0..898b322c 100644 --- a/roseau/load_flow/utils/__init__.py +++ b/roseau/load_flow/utils/__init__.py @@ -1,8 +1,7 @@ """ This module contains utility classes and functions for Roseau Load Flow. """ -from roseau.load_flow.utils.console import console, palette -from roseau.load_flow.utils.constants import CX, DELTA_P, EPSILON_0, EPSILON_R, MU_0, MU_R, OMEGA, PI, RHO, TAN_D, F +from roseau.load_flow.utils.constants import DELTA_P, EPSILON_0, EPSILON_R, MU_0, MU_R, OMEGA, PI, RHO, TAN_D, F from roseau.load_flow.utils.mixins import CatalogueMixin, Identifiable, JsonMixin from roseau.load_flow.utils.types import ( BranchTypeDtype, @@ -15,7 +14,6 @@ __all__ = [ # Constants - "CX", "DELTA_P", "EPSILON_0", "EPSILON_R", @@ -38,7 +36,4 @@ "PhaseDtype", "VoltagePhaseDtype", "BranchTypeDtype", - # Console - "console", - "palette", ] diff --git a/roseau/load_flow/utils/console.py b/roseau/load_flow/utils/console.py deleted file mode 100644 index c16e9c7f..00000000 --- a/roseau/load_flow/utils/console.py +++ /dev/null @@ -1,25 +0,0 @@ -from rich.console import Console - -console = Console() - -palette = [ - "#4c72b0", - "#dd8452", - "#55a868", - "#c44e52", - "#8172b3", - "#937860", - "#da8bc3", - "#8c8c8c", - "#ccb974", - "#64b5cd", -] -"""Color palette for the catalogue tables. - -This is seaborn's default color palette. Generated with: -```python -import seaborn as sns -sns.set_theme() -list(sns.color_palette().as_hex()) -``` -""" diff --git a/roseau/load_flow/utils/constants.py b/roseau/load_flow/utils/constants.py index c1d7b586..dc59b3c6 100644 --- a/roseau/load_flow/utils/constants.py +++ b/roseau/load_flow/utils/constants.py @@ -1,71 +1,77 @@ import numpy as np from roseau.load_flow.units import Q_ -from roseau.load_flow.utils.types import ConductorType, InsulatorType, LineType +from roseau.load_flow.utils.types import ConductorType, InsulatorType PI = np.pi -"""The famous constant :math:`\\pi`.""" +"""The famous mathematical constant :math:`\\pi = 3.141592\\ldots`.""" MU_0 = Q_(1.25663706212e-6, "H/m") -"""Magnetic permeability of the vacuum (H/m).""" +"""Magnetic permeability of the vacuum :math:`\\mu_0 = 4 \\pi \\times 10^{-7}` (H/m).""" EPSILON_0 = Q_(8.8541878128e-12, "F/m") -"""Permittivity of the vacuum (F/m).""" +"""Vacuum permittivity :math:`\\varepsilon_0 = 8.8541878128 \\times 10^{-12}` (F/m).""" F = Q_(50.0, "Hz") -"""Network frequency :math:`=50` (Hz).""" +"""Network frequency :math:`f = 50` (Hz).""" OMEGA = Q_(2 * PI * F, "rad/s") -"""Pulsation :math:`\\omega = 2 \\pi f` (rad/s).""" +"""Angular frequency :math:`\\omega = 2 \\pi f` (rad/s).""" RHO = { - ConductorType.CU: Q_(1.72e-8, "ohm*m"), - ConductorType.AL: Q_(2.82e-8, "ohm*m"), - ConductorType.AM: Q_(3.26e-8, "ohm*m"), - ConductorType.AA: Q_(4.0587e-8, "ohm*m"), + ConductorType.CU: Q_(1.7241e-8, "ohm*m"), # IEC 60287-1-1 Table 1 + ConductorType.AL: Q_(2.8264e-8, "ohm*m"), # IEC 60287-1-1 Table 1 + ConductorType.AM: Q_(3.26e-8, "ohm*m"), # verified + ConductorType.AA: Q_(4.0587e-8, "ohm*m"), # verified (approx. AS 3607 ACSR/GZ) ConductorType.LA: Q_(3.26e-8, "ohm*m"), } -"""Resistivity of common conductor materials (ohm.m).""" - -CX = { - LineType.OVERHEAD: Q_(0.35, "ohm/km"), - LineType.UNDERGROUND: Q_(0.1, "ohm/km"), - LineType.TWISTED: Q_(0.1, "ohm/km"), -} -"""Reactance parameter for a typical line in France (Ohm/km).""" +"""Resistivity of common conductor materials (Ohm.m).""" MU_R = { - ConductorType.CU: Q_(1.2566e-8, "H/m"), - ConductorType.AL: Q_(1.2566e-8, "H/m"), - ConductorType.AM: Q_(1.2566e-8, "H/m"), - ConductorType.AA: Q_(np.nan, "H/m"), # TODO - ConductorType.LA: Q_(np.nan, "H/m"), # TODO + ConductorType.CU: Q_(0.9999935849131266), + ConductorType.AL: Q_(1.0000222328028834), + ConductorType.AM: Q_(0.9999705074463784), + ConductorType.AA: Q_(1.0000222328028834), # ==AL + ConductorType.LA: Q_(0.9999705074463784), # ==AM } -"""Magnetic permeability of common conductor materials (H/m).""" +"""Relative magnetic permeability of common conductor materials.""" DELTA_P = { - ConductorType.CU: Q_(9.3, "mm"), - ConductorType.AL: Q_(112, "mm"), - ConductorType.AM: Q_(12.9, "mm"), - ConductorType.AA: Q_(np.nan, "mm"), # TODO - ConductorType.LA: Q_(np.nan, "mm"), # TODO + ConductorType.CU: Q_(9.33, "mm"), + ConductorType.AL: Q_(11.95, "mm"), + ConductorType.AM: Q_(12.85, "mm"), + ConductorType.AA: Q_(14.34, "mm"), + ConductorType.LA: Q_(12.85, "mm"), } -"""Skin effect of common conductor materials (mm).""" +"""Skin depth of common conductor materials :math:`\\sqrt{\\dfrac{\\rho}{\\pi f \\mu_r \\mu_0}}` (mm).""" +# Skin depth is the depth at which the current density is reduced to 1/e (~37%) of the surface value. +# Generated with: +# --------------- +# def delta_p(rho, mu_r): +# return np.sqrt(rho / (PI * F * mu_r * MU_0)) +# for material in ConductorType: +# print(material, delta_p(RHO[material], MU_R[material]).m_as("mm")) TAN_D = { - InsulatorType.PVC: Q_(600e-4), - InsulatorType.HDPE: Q_(6e-4), - InsulatorType.LDPE: Q_(6e-4), - InsulatorType.PEX: Q_(30e-4), - InsulatorType.EPR: Q_(125e-4), + InsulatorType.PVC: Q_(1000e-4), + InsulatorType.HDPE: Q_(10e-4), + InsulatorType.MDPE: Q_(10e-4), + InsulatorType.LDPE: Q_(10e-4), + InsulatorType.XLPE: Q_(40e-4), + InsulatorType.EPR: Q_(200e-4), + InsulatorType.IP: Q_(100e-4), } -"""Loss angles of common insulator materials.""" +"""Loss angles of common insulator materials according to the IEC 60287 standard.""" +# IEC 60287-1-1 Table 3. We only include the MV values. EPSILON_R = { - InsulatorType.PVC: Q_(6.5), + InsulatorType.PVC: Q_(8), InsulatorType.HDPE: Q_(2.3), - InsulatorType.LDPE: Q_(2.2), - InsulatorType.PEX: Q_(2.5), - InsulatorType.EPR: Q_(3.1), + InsulatorType.MDPE: Q_(2.3), + InsulatorType.LDPE: Q_(2.3), + InsulatorType.XLPE: Q_(2.5), + InsulatorType.EPR: Q_(3), + InsulatorType.IP: Q_(4), } -"""Relative permittivity of common insulator materials.""" +"""Relative permittivity of common insulator materials according to the IEC 60287 standard.""" +# IEC 60287-1-1 Table 3. We only include the MV values. diff --git a/roseau/load_flow/utils/log.py b/roseau/load_flow/utils/log.py index d4563ac8..624207c2 100644 --- a/roseau/load_flow/utils/log.py +++ b/roseau/load_flow/utils/log.py @@ -1,34 +1,7 @@ from typing import Literal -from rich.console import Console - from roseau.load_flow_engine.cy_engine import cy_set_logging_config -# Rich console -console = Console() - -palette = [ - "#4c72b0", - "#dd8452", - "#55a868", - "#c44e52", - "#8172b3", - "#937860", - "#da8bc3", - "#8c8c8c", - "#ccb974", - "#64b5cd", -] -"""Color palette for the catalogue tables. - -This is seaborn's default color palette. Generated with: -```python -import seaborn as sns -sns.set_theme() -list(sns.color_palette().as_hex()) -``` -""" - def set_logging_config(verbosity: Literal["trace", "debug", "info", "warning", "error", "critical"]) -> None: """Configure the logging level of the solver. diff --git a/roseau/load_flow/utils/mixins.py b/roseau/load_flow/utils/mixins.py index af01846c..99afd138 100644 --- a/roseau/load_flow/utils/mixins.py +++ b/roseau/load_flow/utils/mixins.py @@ -1,10 +1,13 @@ import json import logging import re +import textwrap from abc import ABCMeta, abstractmethod +from collections.abc import Sequence from pathlib import Path -from typing import Generic, TypeVar +from typing import Generic, NoReturn, TypeVar, overload +import pandas as pd from typing_extensions import Self from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode @@ -173,12 +176,101 @@ def from_catalogue(cls, **kwargs) -> Self: """ raise NotImplementedError - @classmethod - @abstractmethod - def print_catalogue(cls, **kwargs) -> None: - """Print the catalogue. + @overload + @staticmethod + def _filter_catalogue_str(value: str | re.Pattern[str], strings: pd.Series) -> "pd.Series[bool]": + ... - Keyword Args: - Arguments that can be used to filter the printed part of the catalogue. + @overload + @staticmethod + def _filter_catalogue_str(value: str | re.Pattern[str], strings: list[str]) -> list[str]: + ... + + @staticmethod + def _filter_catalogue_str( + value: str | re.Pattern[str], strings: list[str] | pd.Series + ) -> "pd.Series[bool] | list[str]": + """Filter the catalogue using a string/regexp value. + + Args: + value: + The string or regular expression to use as a filter. + + strings: + The catalogue data to filter. Either a :class:`pandas.Series` or a list of strings. + + Returns: + The mask of matching results if `strings` is a :class:`pandas.Series`, otherwise + the list of matching results. """ - raise NotImplementedError + vector = pd.Series(strings) + if isinstance(value, re.Pattern): + result = vector.str.match(value) + else: + try: + pattern = re.compile(pattern=value, flags=re.IGNORECASE) + result = vector.str.match(pattern) + except re.error: + # fallback to string comparison + result = vector.str.lower() == value.lower() + if isinstance(strings, pd.Series): + return result + else: + return vector[result].tolist() + + @staticmethod + def _raise_not_found_in_catalogue( + value: object, name: str, name_plural: str, strings: pd.Series, query_msg_list: list[str] + ) -> NoReturn: + """Raise an exception when no element has been found in the catalogue. + + Args: + value: + The value that has been searched in the catalogue. + + name: + The name of the element to display in the error message. + + name_plural: + The plural form of the name of the element to display in the error message. + + strings: + The catalogue data to filter. + + query_msg_list: + The query information to display in the error message. + """ + available_values = textwrap.shorten(", ".join(map(repr, strings.unique().tolist())), width=500) + msg = f"No {name} matching {value} has been found" + if query_msg_list: + msg += f" for the query {', '.join(query_msg_list)}" + msg += f". Available {name_plural} are {available_values}." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) + + @staticmethod + def _assert_one_found(found_data: Sequence[object], display_name: str, query_info: str) -> None: + """Assert that only one element has been found in the catalogue. + + Args: + found_data: + The data found in the catalogue. If multiple elements have been found, they are + displayed in the error message. + + display_name: + The name of the element to display in the error message. + + query_info: + The query information to display in the error message. + """ + if len(found_data) == 1: + return + msg_middle = f"{display_name} matching the query ({query_info}) have been found" + if len(found_data) == 0: + msg = f"No {msg_middle}. Please look at the catalogue using the `get_catalogue` class method." + code = RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + else: + msg = f"Several {msg_middle}: {textwrap.shorten(', '.join(map(repr, found_data)), width=500)}." + code = RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=code) diff --git a/roseau/load_flow/utils/tests/test_types.py b/roseau/load_flow/utils/tests/test_types.py index 5e0b7f31..e38591a3 100644 --- a/roseau/load_flow/utils/tests/test_types.py +++ b/roseau/load_flow/utils/tests/test_types.py @@ -10,39 +10,40 @@ @pytest.mark.parametrize(scope="module", argnames="t", argvalues=TYPES, ids=TYPES_IDS) def test_types_basic(t): for x in t: - assert t.from_string(str(x)) == x + assert t(str(x)) == x assert "." not in str(x) def test_line_type(): with pytest.raises(RoseauLoadFlowException) as e: - LineType.from_string("") - assert "cannot be converted into a LineType" in e.value.args[0] - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_LINE_TYPE + LineType("") + assert "cannot be converted into a LineType" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_LINE_TYPE with pytest.raises(RoseauLoadFlowException) as e: - LineType.from_string("nan") - assert "cannot be converted into a LineType" in e.value.args[0] - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_LINE_TYPE + LineType("nan") + assert "cannot be converted into a LineType" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_LINE_TYPE - assert LineType.from_string("Aérien") == LineType.OVERHEAD - assert LineType.from_string("Aerien") == LineType.OVERHEAD - assert LineType.from_string("galerie") == LineType.OVERHEAD - assert LineType.from_string("Souterrain") == LineType.UNDERGROUND - assert LineType.from_string("torsadé") == LineType.TWISTED - assert LineType.from_string("Torsade") == LineType.TWISTED + assert LineType("oVeRhEaD") == LineType.OVERHEAD + assert LineType("o") == LineType.OVERHEAD + assert LineType("uNdErGrOuNd") == LineType.UNDERGROUND + assert LineType("u") == LineType.UNDERGROUND + assert LineType("tWiStEd") == LineType.TWISTED + assert LineType("T") == LineType.TWISTED def test_insulator_type(): - assert InsulatorType.from_string("") == InsulatorType.UNKNOWN - assert InsulatorType.from_string("nan") == InsulatorType.UNKNOWN + assert InsulatorType("") == InsulatorType.UNKNOWN + assert InsulatorType("nan") == InsulatorType.UNKNOWN + assert InsulatorType("pex") == InsulatorType.XLPE def test_conductor_type(): with pytest.raises(RoseauLoadFlowException) as e: - ConductorType.from_string("") - assert "cannot be converted into a ConductorType" in e.value.args[0] - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE + ConductorType("") + assert "cannot be converted into a ConductorType" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE with pytest.raises(RoseauLoadFlowException) as e: - ConductorType.from_string("nan") - assert "cannot be converted into a ConductorType" in e.value.args[0] - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE + ConductorType("nan") + assert "cannot be converted into a ConductorType" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE diff --git a/roseau/load_flow/utils/types.py b/roseau/load_flow/utils/types.py index e792f510..e1c63440 100644 --- a/roseau/load_flow/utils/types.py +++ b/roseau/load_flow/utils/types.py @@ -1,9 +1,9 @@ import logging -from enum import Enum, auto, unique +from enum import auto import pandas as pd -from typing_extensions import Self +from roseau.load_flow._compat import StrEnum from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode # The local logger @@ -52,198 +52,129 @@ } -@unique -class LineType(Enum): +class LineType(StrEnum): """The type of a line.""" OVERHEAD = auto() - """The line is an overhead line.""" + """An overhead line that can be vertically or horizontally configured -- Fr = Aérien.""" UNDERGROUND = auto() - """The line is an underground line.""" + """An underground or a submarine cable -- Fr = Souterrain/Sous-Marin.""" TWISTED = auto() - """The line is a twisted line.""" + """A twisted line commonly known as Aerial Cable or Aerial Bundled Conductor (ABC) -- Fr = Torsadé.""" - def __str__(self) -> str: - """Print a `LineType` - - Returns: - A printable string of the line type. - """ - return self.name.lower() + # aliases + O = OVERHEAD # noqa: E741 + U = UNDERGROUND + T = TWISTED @classmethod - def from_string(cls, string: str) -> Self: - """Convert a string into a LineType - - Args: - string: - The string to convert - - Returns: - The corresponding LineType. - """ - string = string.lower() - if string in ("overhead", "aérien", "aerien", "galerie", "a", "o"): - return cls.OVERHEAD - elif string in ("underground", "souterrain", "sous-marin", "s", "u"): - return cls.UNDERGROUND - elif string in ("twisted", "torsadé", "torsade", "t"): - return cls.TWISTED - else: - msg = f"The string {string!r} cannot be converted into a LineType." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE) - - # - # WordingCodeMixin - # - def code(self) -> str: - """The code method is modified to retrieve a code that can be used in line type names. - - Returns: - The code of the enumerated value. - """ - if self == LineType.OVERHEAD: - return "O" - elif self == LineType.UNDERGROUND: - return "U" - elif self == LineType.TWISTED: - return "T" - else: # pragma: no cover - msg = f"There is code missing here. I do not know the LineType {self!r}." - logger.error(msg) - raise NotImplementedError(msg) + def _missing_(cls, value: object) -> "LineType | None": + if isinstance(value, str): + try: + return cls[value.upper()] + except KeyError: + pass + msg = f"{value!r} cannot be converted into a LineType." + logger.error(msg) + raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.BAD_LINE_TYPE) - -# Add the list of codes for each line type -LineType.CODES = {LineType.OVERHEAD: {"A", "O"}, LineType.UNDERGROUND: {"U", "S"}, LineType.TWISTED: {"T"}} + def code(self) -> str: + """A code that can be used in line type names.""" + return self.name[0] -@unique -class ConductorType(Enum): - """The type of conductor.""" +class ConductorType(StrEnum): + """The type of the material of the conductor.""" - AL = auto() - """The conductor is in Aluminium.""" CU = auto() - """The conductor is in Copper.""" + """Copper -- Fr = Cuivre.""" + AL = auto() + """All Aluminum Conductor (AAC) -- Fr = Aluminium.""" AM = auto() - """The conductor is in Almélec.""" + """All Aluminum Alloy Conductor (AAAC) -- Fr = Almélec.""" AA = auto() - """The conductor is in Alu-Acier.""" + """Aluminum Conductor Steel Reinforced (ACSR) -- Fr = Alu-Acier.""" LA = auto() - """The conductor is in Almélec-Acier.""" - - def __str__(self) -> str: - """Print a `ConductorType` - - Returns: - A printable string of the conductor type. - """ - if self == ConductorType.AL: - return "Al" - elif self == ConductorType.CU: - return "Cu" - elif self == ConductorType.AM: - return "AM" - elif self == ConductorType.AA: - return "AA" - elif self == ConductorType.LA: - return "LA" - else: - s = super().__str__() - msg = f"The ConductorType {s} is not known..." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE) + """Aluminum Alloy Conductor Steel Reinforced (AACSR) -- Fr = Almélec-Acier.""" + + # Aliases + AAC = AL # 1350-H19 (Standard Round of Compact Round) + """All Aluminum Conductor (AAC) -- Fr = Aluminium.""" + # AAC/TW # 1380-H19 (Trapezoidal Wire) + + AAAC = AM + """All Aluminum Alloy Conductor (AAAC) -- Fr = Almélec.""" + # Aluminum alloy 6201-T81. + # Concentric-lay-stranded + # conforms to ASTM Specification B-399 + # Applications: Overhead + + ACSR = AA + """Aluminum Conductor Steel Reinforced (ACSR) -- Fr = Alu-Acier.""" + # Aluminum alloy 1350-H-19 + # Applications: Bare overhead transmission cable and primary and secondary distribution cable + + AACSR = LA + """Aluminum Alloy Conductor Steel Reinforced (AACSR) -- Fr = Almélec-Acier.""" @classmethod - def from_string(cls, string: str) -> Self: - """Convert a string into a ConductorType - - Args: - string: - The string to convert - - Returns: - The corresponding ConductorType. - """ - string = string.lower() - if string == "al": - return cls.AL - elif string == "cu": - return cls.CU - elif string == "am": - return cls.AM - elif string == "aa": - return cls.AA - elif string == "la": - return cls.LA - else: - msg = f"The string {string!r} cannot be converted into a ConductorType." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE) - - # - # WordingCodeMixin - # - def code(self) -> str: - """The code method is modified to retrieve a code that can be used in line type names. + def _missing_(cls, value: object) -> "ConductorType | None": + if isinstance(value, str): + try: + return cls[value.upper()] + except KeyError: + pass + msg = f"{value!r} cannot be converted into a ConductorType." + logger.error(msg) + raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE) - Returns: - The code of the enumerated value. - """ - return self.name.upper() + def code(self) -> str: + """A code that can be used in conductor type names.""" + return self.name -@unique -class InsulatorType(Enum): +class InsulatorType(StrEnum): """The type of the insulator for a wire.""" UNKNOWN = auto() - """The insulator of the conductor is made with unknown material.""" + """The material of the insulator is unknown.""" + + # General insulators (IEC 60287) HDPE = auto() - """The insulator of the conductor is made with High-Density PolyEthylene.""" + """High-Density PolyEthylene (HDPE) insulation.""" + MDPE = auto() + """Medium-Density PolyEthylene (MDPE) insulation.""" LDPE = auto() - """The insulator of the conductor is made with Low-Density PolyEthylene.""" - PEX = auto() - """The insulator of the conductor is made with Cross-linked polyethylene.""" + """Low-Density PolyEthylene (LDPE) insulation.""" + XLPE = auto() + """Cross-linked polyethylene (XLPE) insulation.""" EPR = auto() - """The insulator of the conductor is made with Ethylene-Propylene Rubber.""" + """Ethylene-Propylene Rubber (EPR) insulation.""" PVC = auto() - """The insulator of the conductor is made with PolyVinyl Chloride.""" - - def __str__(self) -> str: - """Print a `InsulatorType` + """PolyVinyl Chloride (PVC) insulation.""" + IP = auto() + """Impregnated Paper (IP) insulation.""" - Returns: - A printable string of the insulator type. - """ - return self.name.upper() + # Aliases + PEX = XLPE + """Alias -- Cross-linked polyethylene (XLPE) insulation.""" + PE = MDPE + """Alias -- Medium-Density PolyEthylene (MDPE) insulation.""" @classmethod - def from_string(cls, string: str) -> Self: - """Convert a string into a InsulatorType - - Args: - string: - The string to convert - - Returns: - The corresponding InsulatorType. - """ - if string.lower() in ("", "unknown", "nan"): - return cls.UNKNOWN - elif string == "HDPE": - return cls.HDPE - elif string == "LDPE": - return cls.LDPE - elif string == "PEX": - return cls.PEX - elif string == "EPR": - return cls.EPR - elif string == "PVC": - return cls.PVC - else: - msg = f"The string {string!r} cannot be converted into a InsulatorType." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_INSULATOR_TYPE) + def _missing_(cls, value: object) -> "InsulatorType | None": + if isinstance(value, str): + string = value.upper() + if string in {"", "NAN"}: + return cls.UNKNOWN + try: + return cls[string] + except KeyError: + pass + msg = f"{value!r} cannot be converted into a InsulatorType." + logger.error(msg) + raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.BAD_INSULATOR_TYPE) + + def code(self) -> str: + """A code that can be used in insulator type names.""" + return self.name