Metadata-Version: 2.4
Name: transformer-thermal-model
Version: 0.4.0
Summary: Thermal model for transformers
License-File: LICENSE
License-File: LICENSES/CC-BY-4.0.txt
License-File: LICENSES/MPL-2.0.txt
Author: Contributors to the Transformer Thermal Model project
Requires-Python: >=3.11, <4.0
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Dist: numpy (>=2.2.5,<3)
Requires-Dist: pandas (>=2.0,<3.0)
Requires-Dist: pydantic (>=2.11.1,<3)
Description-Content-Type: text/markdown

<!--
SPDX-FileCopyrightText: Contributors to the Transformer Thermal Model project

SPDX-License-Identifier: MPL-2.0
-->
# Transformer thermal model

[![PyPI version](https://badge.fury.io/py/transformer-thermal-model.svg?no-cache)](https://badge.fury.io/py/transformer-thermal-model.svg) <!-- markdownlint-disable-line first-line-h1 line-length -->
[![License: MPL2.0](https://img.shields.io/badge/License-MPL2.0-informational.svg)](https://github.com/alliander-opensource/transformer-thermal-model/blob/main/LICENSE)
[![Downloads](https://static.pepy.tech/badge/transformer-thermal-model)](https://pepy.tech/project/transformer-thermal-model)
[![Downloads](https://static.pepy.tech/badge/transformer-thermal-model/month)](https://pepy.tech/project/transformer-thermal-model)
[![DOI](https://zenodo.org/badge/984616930.svg)](https://doi.org/10.5281/zenodo.17434808)

[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=alliander-opensource_transformer-thermal-model&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=alliander-opensource_transformer-thermal-model)
[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=alliander-opensource_transformer-thermal-model&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=alliander-opensource_transformer-thermal-model)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=alliander-opensource_transformer-thermal-model&metric=coverage)](https://sonarcloud.io/summary/new_code?id=alliander-opensource_transformer-thermal-model)
[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=alliander-opensource_transformer-thermal-model&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=alliander-opensource_transformer-thermal-model)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=alliander-opensource_transformer-thermal-model&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=alliander-opensource_transformer-thermal-model)

`transformer-thermal-model` is a library for modelling the transformer top-oil and
hot-spot temperature based on the transformer specifications, a load profile and an ambient temperature profile.
The model is an implementation according to the standard IEC 60076-7, also known as de Loading Guide.

Check out our [documentation page](https://alliander-opensource.github.io/transformer-thermal-model/),
the [technical documentation](https://alliander-opensource.github.io/transformer-thermal-model/theoretical_documentation/overview/),
the [quick start](https://alliander-opensource.github.io/transformer-thermal-model/examples/quickstart/)
or one of the many other examples!

## Installation

### Install from PyPI

You can directly install the package from PyPI.

``` bash
pip install transformer-thermal-model
```

## Structure of the code

This package contains the following modules to work with:

- `transformer_thermal_model.model`: the core of the package to calculate a temperature profile of a transformer using
the thermal model;
- `transformer_thermal_model.transformer`: a module containing multiple
  transformer classes to be used to define a transformer;
- `transformer_thermal_model.cooler`: a module containing the enumerators to define the cooler type of the transformer;
- `transformer_thermal_model.schemas`: in schemas one can find a.o. the definition of the model interfaces and how the
transformer specificitations should be specified;
- `transformer_thermal_model.hot_spot_calibration`: a module that is used to
determine the transformer hot-spot factor to be used in the thermal model.

The following modules contain elements that are not required for thermal modelling but are used to get more insight in
aging and load capacity:

- `transformer_thermal_model.aging`: a module to calculate the aging of a transformer according to IEC 60076-7
paragraph 6.3;
- `transformer_thermal_model.components`: a module containing enumerators to define components of a power transformer if
one wants to calculate the relative load capacity of each component in the transformer.

## How to create a transformer

Before performing a calculation, a transformer object must be created with all
necessary details about the transformer.  Default values will be used for any
attribute not specified by the user.

`transformer_thermal_model.transformer`: This module has multiple `Transformer`
children, which will be elements used for the temperature calculation inside `Model`.

- `PowerTransformer`: A power transformer class, child of the `Transformer`
  class.
- `DistributionTransformer`: A distribution transformer class, also child of
  the `Transformer` class.
- `ThreeWindingTransformer`: A three winding transformer, also child of
  the `Transformer` class.
- `TransformerType` (`Enum`): For easily checking all available types. Does not
   have any use in our code, but might be useful for your use-case.

### Transformer specifications

A `Transformer` needs a couple of specifications (`user_specs`) and a
cooling type (in `cooling_type`). The specifications are defined in
`transformer_thermal_model.schemas.UserTransformerSpecifications`, and are
a composition of some mandatory specifications and some optionals. When the
optionals are left unchanged, the default values per class will be used.
These can be found by `defaults`, e.g. `PowerTransformer.defaults` will show
you the default specifications.

An example on how to initialise a `PowerTransformer`:

```Python
from transformer_thermal_model.cooler import CoolerType
from transformer_thermal_model.transformer import PowerTransformer
from transformer_thermal_model.schemas import UserTransformerSpecifications

tr_specs = UserTransformerSpecifications(
   load_loss=1000,  # Transformer load loss [W]
   nom_load_sec_side=1500,  # Transformer nominal current secondary side [A]
   no_load_loss=200,  # Transformer no-load loss [W]
   amb_temp_surcharge=20,  # Ambient temperature surcharge [K]
)
transformer = PowerTransformer(user_specs=tr_specs, cooling_type=CoolerType.ONAF)
```

It is recommended to adjust the following parameters when modelling a specific
transformer as these influence the thermal modelling and are
transformer-specifically defined (_the ones with a '*' are mandatory_):

- `load_loss`*: load loss of the transformer.
- `no_load_loss`*: no load loss of the transformer.
- `nom_load_sec_side`*: nominal current of the secondary side of
  the transformer.
- `amb_temp_surcharge`*:
  - `PowerTransformer`: temperature surcharge on ambient temperature due to
    installation conditions.
  - `DistributionTransformer`: Building or placement thermal classification.
- `top_oil_temp_rise`: top-oil temperature increase under
  nominal conditions.
- `winding_oil_gradient`: temperature difference between the average
winding and average oil temperature under nominal conditions
- `hot_spot_fac`: factor determining the temperature difference
between top-oil and hot-spot. Only specify when known, otherwise see [hot-spot
factor calibration](#hot-spot-factor-calibration-for-power-transformers).

#### Cooler

`transformer_thermal_model.cooler`: Similarly, you must provide the type of
cooling of your transformer. This is only for a `PowerTransformer`, since a
`DistributionTransformer` is always ONAN.
To adjust the cooler type for a `PowerTransformer`, you can do the following:

```Python
from transformer_thermal_model.transformer import PowerTransformer
from transformer_thermal_model.cooler import CoolerType
from transformer_thermal_model.schemas import UserTransformerSpecifications

tr_specs = UserTransformerSpecifications(
                load_loss=10000,
                nom_load_sec_side=1000,
                no_load_loss=1000,
                amb_temp_surcharge=0,
            )

# You can create a power transformer with ONAF cooling.
onaf_trafo = PowerTransformer(user_specs = tr_specs, cooling_type = CoolerType.ONAF)
# Or you can create a power transformer with ONAN cooling
onan_trafo = PowerTransformer(user_specs = tr_specs, cooling_type = CoolerType.ONAN)
```

#### Hot-spot factor calibration for power transformers

When the hot-spot factor of a transformer is known, it can be given as a
specification as `hot_spot_fact` in a `UserTransformerSpecifications`
object.

It often occurs, however, that a transformer hot-spot factor is not
known. If this is the case, a hot-spot factor calibration can be performed to determine
the hot-spot factor of a given `PowerTransformer` object. Note that for
`DistributionTransformer` objects, hot-spot calibrations _**should not be performed**_ and the
default value can be used. For both the `PowerTransformer` and the `DistributionTransformer` the `hot_spot_fac`
in the specification is set to the default value of no hot-spot factor is provided via the `UserTransformerSpecifications`.

The following example shows how to calibrate a `PowerTransformer` object.

```Python
from transformer_thermal_model.cooler import CoolerType
from transformer_thermal_model.schemas import UserTransformerSpecifications
from transformer_thermal_model.transformer import PowerTransformer
from transformer_thermal_model.hot_spot_calibration import calibrate_hotspot_factor

tr_specs = UserTransformerSpecifications(
   load_loss=1000,  # Transformer load loss [W]
   nom_load_sec_side=1500,  # Transformer nominal current secondary side [A]
   no_load_loss=200,  # Transformer no-load loss [W]
   amb_temp_surcharge=20,  # Ambient temperature surcharge [K]
)
uncalibrated_transformer = PowerTransformer(user_specs=tr_specs, cooling_type=CoolerType.ONAF)
calibrated_trafo = calibrate_hotspot_factor(
   uncalibrated_transformer=uncalibrated_transformer,
   ambient_temp=20.0,
   hot_spot_limit=98, # in most cases a hot-spot temperature limit of 98 can be used
   hot_spot_factor_min=1.1,
   hot_spot_factor_max=1.3,
)
```

The new hot-spot factor is available via `calibrated_trafo.specs.hot_spot_fac`, and from now on used in the thermal model.

```text
>>> print(calibrated_trafo.specs.hot_spot_fac)
1.1
```

#### Component capacities of a power transformer

For load capacity calculations it can be required to take into account the capacity of the different components of a
power transformers.
Therefore a functionality is available to calculate the relative capacity of the tap changers, the bushings (primary and
secondary) and the internal current transformer. The capacity is defined as the ratio of the component capacity and the
nominal load of the transformer.

The capacities are properties of the `PowerTransformer` and require the `TransformerComponentSpecifications` to be given
during initiating a PowerTransformer object.

In the following example for all components the required information is provided.

``` python
from transformer_thermal_model.components import BushingConfig, TransformerSide, VectorConfig
from transformer_thermal_model.cooler import CoolerType
from transformer_thermal_model.schemas import (
    TransformerComponentSpecifications,
    UserTransformerSpecifications,
)
from transformer_thermal_model.transformer import PowerTransformer

comp_specs = TransformerComponentSpecifications(
        tap_chang_capacity=600,  # Tap changer nominal current [A]
        nom_load_prim_side=550,  # Transformer nominal current primary side [A]
        tap_chang_conf=VectorConfig.TRIANGLE_OUTSIDE,  # Tap Changer configuration
        tap_chang_side=TransformerSide.SECONDARY,  # Tap changer side
        prim_bush_capacity=600,  # Primary bushing nominal current [A]
        prim_bush_conf=BushingConfig.SINGLE_BUSHING,  # Primary bushing configuration
        sec_bush_capacity=1800,  # Secondary bushing nominal current [A]
        sec_bush_conf=BushingConfig.SINGLE_BUSHING,  # Secondary bushing configuration
        cur_trans_capacity=1300,  # Current transformer nominal current [A]
        cur_trans_conf=VectorConfig.STAR,  # Current transformer configuration
        cur_trans_side=TransformerSide.PRIMARY,  # Current transformer side
    )
user_specs = UserTransformerSpecifications(
        load_loss=1000,  # Transformer load loss [W]
        nom_load_sec_side=1500,  # Transformer nominal current secondary side [A]
        no_load_loss=200,  # Transformer no-load loss [W]
        amb_temp_surcharge=20,
    )
power_transformer = PowerTransformer(
        user_specs=user_specs, cooling_type=CoolerType.ONAF, internal_component_specs=comp_specs
    )
```

The resulting component capacities are available in `power_transformer.component_capacities`:

```text
>>> print(power_transformer.component_capacities)
{'tap_changer': 0.4, 'primary_bushings': 1.0909090909090908, 'secondary_bushings': 1.2, 'current_transformer': 2.3636363636363638}
```

Note that it is also possible to
only define a subset of the components if not all components are present. Then only the component capacities of the
provided components are available:

``` python
comp_specs = TransformerComponentSpecifications(
    tap_chang_capacity=600,  # Tap changer nominal current [A]
    nom_load_prim_side=550,  # Transformer nominal current primary side [A]
    tap_chang_conf=VectorConfig.TRIANGLE_OUTSIDE,  # Tap Changer configuration
    tap_chang_side=TransformerSide.SECONDARY,  # Tap changer side
)
power_transformer = PowerTransformer(
        user_specs=user_specs, cooling_type=CoolerType.ONAF, internal_component_specs=comp_specs
    )
```

This will generate the following result:

``` text
>>> print(power_transformer.component_capacities)
{'tap_changer': 0.4, 'primary_bushings': None, 'secondary_bushings': None, 'current_transformer': None}
```

### Thermal modelling

When a `Transformer` object is completely defined, the temperatures can be calculated using
`transformer_thermal_model.model`: the core of the package. The `Model` is
built as follows:

```Python
import pandas as pd

from transformer_thermal_model.model import Model
from transformer_thermal_model.cooler import CoolerType
from transformer_thermal_model.schemas import UserTransformerSpecifications, InputProfile
from transformer_thermal_model.transformer import PowerTransformer

# In this example the model is used to calculate the transformer temperature based on a load and ambient
# profile with a period of one week. Any duration can be chosen preferably with timestamps with an interval of
# 15 minute or lower. Larger timesteps will result in incorrect results but it *is* possible to calculate with them.
one_week = 4*24*7
datetime_index = pd.date_range("2020-01-01", periods=one_week, freq="15min")

# For the load (in A) and ambient temperature (in C) arbitrary constants profiles are chosen.
# It is also possible to use a realistic profile.
nominal_load = 100
load_points = pd.Series([nominal_load] * one_week, index=datetime_index)
ambient_temp = 21
temperature_points = pd.Series([ambient_temp] * one_week, index=datetime_index)

# Create an input object with the profiles
profile_input = InputProfile.create(
   datetime_index = datetime_index,
   load_profile = load_points,
   ambient_temperature_profile = temperature_points
)

# Initialise a power transformer with cooling type ONAF and, besides the mandatory user specifications, default values.
tr_specs = UserTransformerSpecifications(
   load_loss=1000,  # Transformer load loss [W]
   nom_load_sec_side=1500,  # Transformer nominal current secondary side [A]
   no_load_loss=200,  # Transformer no-load loss [W]
   amb_temp_surcharge=20,  # Ambient temperature surcharge [K]
)
transformer = PowerTransformer(user_specs=tr_specs, cooling_type=CoolerType.ONAF)
model = Model(
   temperature_profile = profile_input,
   transformer = transformer
)

results = model.run()

# Get the results as pd.Series, with the same datetime_index as your input.
top_oil_temp_profile = results.top_oil_temp_profile
hot_spot_temp_profile = results.hot_spot_temp_profile
```

```text
>>> top_oil_temp_profile.head(3)
2020-01-01 00:00:00    41.000000
2020-01-01 00:15:00    43.639919
2020-01-01 00:30:00    45.801302

>>> hot_spot_temp_profile.head(3)
2020-01-01 00:00:00    41.000000
2020-01-01 00:15:00    44.381177
2020-01-01 00:30:00    46.443459
```

You can use the `Model` to run a calculation with either a `PowerTransformer` or
a `DistributionTransformer`. The model will return `results` with the
`top_oil_temperature` and `hot_spot_temperature` as `pd.Series`. More
specifically, the output will be in the form of `OutputProfile`, as
defined in `transformer_thermal_model.schemas`. The reason for using pd.Series
as output, is that the model uses the `index` of the series that you have provided to run the
calculation, which also creates the benefit for you that you can
relate the result to your provided input for eventual
cross-validation or analysis.

### Three winding Transformer modelling

The model also supports thermal modeling of three-winding transformers. To use this feature, provide both a
`ThreeWindingTransformer` and a `ThreeWindingInputProfile` as input to the `Model`. The
`ThreeWindingInputProfile` requires a separate load profile for each winding (high, medium, and low voltage sides),
all sharing the same length and datetime index.

With `UserThreeWindingTransformerSpecifications`, you can specify the nominal load and winding oil gradient for each
winding, as well as the load losses between windings. The resulting hot-spot temperature profile is returned as a
DataFrame with columns for each winding, making it easy to analyze the results for each part of the transformer.

```Python
import pandas as pd

from transformer_thermal_model.cooler import CoolerType
from transformer_thermal_model.model import Model
from transformer_thermal_model.schemas import (
    ThreeWindingInputProfile,
    UserThreeWindingTransformerSpecifications,
    WindingSpecifications,
)
from transformer_thermal_model.transformer import ThreeWindingTransformer

# Define the time range for your simulation
one_week = 4 * 24 * 7
datetime_index = pd.date_range("2020-01-01", periods=one_week, freq="15min")

# Create ambient temperature profile (can be a constant or a realistic profile)
ambient_temp = 21
temperature_points = pd.Series([ambient_temp] * one_week, index=datetime_index)

# Create load profiles for each winding (high, medium, low voltage sides)
load_profile_high_voltage_side = pd.Series([2000] * one_week, index=datetime_index)
load_profile_middle_voltage_side = pd.Series([1500] * one_week, index=datetime_index)
load_profile_low_voltage_side = pd.Series([1000] * one_week, index=datetime_index)

# Create the input profile for the three-winding transformer
profile_input = ThreeWindingInputProfile.create(
    datetime_index=datetime_index,
    ambient_temperature_profile=temperature_points,
    load_profile_high_voltage_side=load_profile_high_voltage_side,
    load_profile_middle_voltage_side=load_profile_middle_voltage_side,
    load_profile_low_voltage_side=load_profile_low_voltage_side,
)

# Define the transformer specifications for each winding
user_specs = UserThreeWindingTransformerSpecifications(
    no_load_loss=20,
    amb_temp_surcharge=10,
    lv_winding=WindingSpecifications(
        nom_load=1000, winding_oil_gradient=20, hot_spot_fac=1.2, time_const_winding=1, nom_power=1000
    ),
    mv_winding=WindingSpecifications(
        nom_load=1000, winding_oil_gradient=20, hot_spot_fac=1.2, time_const_winding=1, nom_power=1000
    ),
    hv_winding=WindingSpecifications(
        nom_load=2000, winding_oil_gradient=20, hot_spot_fac=1.2, time_const_winding=1, nom_power=2000
    ),
    load_loss_hv_lv=100,
    load_loss_hv_mv=100,
    load_loss_mv_lv=100,
)

# Create the transformer object
transformer = ThreeWindingTransformer(user_specs=user_specs, cooling_type=CoolerType.ONAN)

# Run the thermal model
model = Model(
    temperature_profile=profile_input,
    transformer=transformer
)
results = model.run()

# The results are returned as pandas Series/DataFrame, indexed by your datetime_index
top_oil_temp_profile = results.top_oil_temp_profile
hot_spot_temp_profile = results.hot_spot_temp_profile

top_oil_temp_profile = results.top_oil_temp_profile
hot_spot_temp_profile = results.hot_spot_temp_profile
```

```text
>>> top_oil_temp_profile.head()
2020-01-01 00:00:00    31.000000
2020-01-01 00:15:00    40.212693
2020-01-01 00:30:00    48.198972
2020-01-01 00:45:00    55.122101
2020-01-01 01:00:00    61.123609

```text
>>> hot_spot_temp_profile.head()
                          low_voltage_side   middle_voltage_side   high_voltage_side
2020-01-01 00:00:00           31.000000           31.000000           31.000000
2020-01-01 00:15:00           84.991214          116.068422           84.991214
2020-01-01 00:30:00           90.234413          119.407866           90.234413
2020-01-01 00:45:00           94.756639          122.263816           94.756639
2020-01-01 01:00:00           98.676844          124.739555           98.676844
```

#### Using the top oil temperature as an input to the model

Optionally, you can provide the top oil temperature as an input parameter to the `InputProfile`
to use it in place of the ambient temperature as an input to the model:

```Python
import pandas as pd

from transformer_thermal_model.model import Model
from transformer_thermal_model.cooler import CoolerType
from transformer_thermal_model.schemas import UserTransformerSpecifications, InputProfile
from transformer_thermal_model.transformer import PowerTransformer

one_week = 4*24*7
datetime_index = pd.date_range("2020-01-01", periods=one_week, freq="15min")

nominal_load = 100
load_points = pd.Series([nominal_load] * one_week, index=datetime_index)
ambient_temp = 21
temperature_points = pd.Series([ambient_temp] * one_week, index=datetime_index)
top_oil_temp = 42
top_oil_points = pd.Series([top_oil_temp] * one_week, index=datetime_index)

profile_input = InputProfile.create(
   datetime_index = datetime_index,
   load_profile = load_points,
   ambient_temperature_profile = temperature_points,
   # Here is where we add the top oil temperature as an input. It has the same shape as the ambient temperature.
   top_oil_temperature_profile = top_oil_points
)

tr_specs = UserTransformerSpecifications(
   load_loss=1000,  # Transformer load loss [W]
   nom_load_sec_side=1500,  # Transformer nominal current secondary side [A]
   no_load_loss=200,  # Transformer no-load loss [W]
   amb_temp_surcharge=20,  # Ambient temperature surcharge [K]
)
transformer = PowerTransformer(user_specs=tr_specs, cooling_type=CoolerType.ONAF)
model = Model(
   temperature_profile = profile_input,
   transformer = transformer
)

# As we have provided the top oil temperature as an input, this is now being used in place of the ambient temperature.
# If you still want to use the top oil temperature you can do so using model.run(force_use_ambient_temperature=True)
results = model.run()

top_oil_temp_profile = results.top_oil_temp_profile
hot_spot_temp_profile = results.hot_spot_temp_profile
```

```text
>>> top_oil_temp_profile.head(3)
2020-01-01 00:00:00    42.0
2020-01-01 00:15:00    42.0
2020-01-01 00:30:00    42.0

>>> hot_spot_temp_profile.head(3)
2020-01-01 00:00:00    42.000000
2020-01-01 00:15:00    42.741258
2020-01-01 00:30:00    42.938711
```

Note, how the top oil temperature we receive as the output `results.top_oil_temp_profile` exactly matches
the top oil temperature we provided as the input.

## License

This project is licensed under the Mozilla Public License, version 2.0 - see
[LICENSE](https://github.com/alliander-opensource/transformer-thermal-model/blob/main/LICENSE) for details.

## Licenses third-party libraries

This project includes third-party libraries,
which are licensed under their own respective Open-Source licenses.
SPDX-License-Identifier headers are used to show which license is applicable.

The concerning license files can be found in the
[LICENSES](https://github.com/alliander-opensource/transformer-thermal-model/blob/main/LICENSES) directory.

## Contributing

Please read
[CODE_OF_CONDUCT](https://github.com/alliander-opensource/transformer-thermal-model/blob/main/CODE_OF_CONDUCT.md),
[CONTRIBUTING](https://github.com/alliander-opensource/transformer-thermal-model/blob/main/CONTRIBUTING.md)
and
[PROJECT GOVERNANCE](https://github.com/alliander-opensource/transformer-thermal-model/blob/main/GOVERNANCE.md)
for details on the process
for submitting pull requests to us.

## Contact

Please read [SUPPORT](https://github.com/alliander-opensource/transformer-thermal-model/blob/main/SUPPORT.md) for how to
get in touch with the Transformer Thermal Model project.

