Vibration Spectrum¶
For more details on vibration data see this dedicated page in the reference section.
VibrationSpectrum(perfdb)
¶
Class used for handling Vibration Spectrum. Can be accessed via perfdb.vibration.spectrum.
Parameters:
Source code in echo_postgres/perfdb_root.py
def __init__(self, perfdb: e_pg.PerfDB) -> None:
"""Base class that all subclasses should inherit from.
Parameters
----------
perfdb : PerfDB
Top level object carrying all functionality and the connection handler.
"""
self._perfdb: e_pg.PerfDB = perfdb
get(period, manufacturer, data_type='Vibration', object_names=None, unit='Order', spectrum_type='Normal', amplitude_type='Peak', sensors=None, acquisition_frequencies=None, variable_names=None, filter_type='and', output_type='DataFrame')
¶
Gets the vibration Spectrum data.
Currently, this will process the data in two different ways depending on the manufacturer of the turbine:
- Gamesa: Time series data will be converted to spectrum using a FFT. If envelope is selected, the Hilbert transform will be used to get the envelope of the signal before the FFT.
- GE: Spectrum data will be used directly, converting the frequency axis to be relative to the generator shaft speed.
The values will be numpy ndarrays with two dimensions [2, :], where first dimension is the frequency (in hertz or orders) and the second is the value.
Assuming array is a value of one row and column "value", if you want to get frequency of the array, you can do array[0, :] and for the value itself array[1, :]
Parameters:
-
(period¶DateTimeRange | list[date]) –Can be a DateTimeRange or a list of dates.
- If DateTimeRange, will get the data for the entire range (limiting on start and end)
- If list of dates, will get the data for each date in the list.
-
(manufacturer¶Literal['Gamesa', 'GE']) –Manufacturer of the wind turbine. Either Gamesa or GE.
-
(data_type¶Literal['Vibration', 'Blade Gap'], default:'Vibration') –Type of the data to get. Can be one of ["Vibration", "Blade Gap"]. If "Vibration", will get vibration data, if "Blade Gap", will get blade gap data. By default "Vibration"
-
(object_names¶list[str] | None, default:None) –Names of the objects to check. If None will check for all objects. By default None
-
(unit¶Literal['Hz', 'Order'], default:'Order') –Unit of the frequency. Can be one of ["Hz", "Order"]. If Order is used, it will always be relative to the HSS/Generator Shaft speed. By default "Order"
-
(spectrum_type¶Literal['Normal', 'Envelope'], default:'Normal') –What kind of spectrum should be returned. By default "Normal"
-
(amplitude_type¶Literal['RMS', 'Peak', 'Peak-to-Peak'], default:'Peak') –Type of amplitude to return. Can be one of ["RMS", "Peak", "Peak-to-Peak"].
For GE turbines only Peak is allowed. Peak-to-Peak and RMS are not supported as the data is acquired directly as spectrum.
By default "Peak"
-
(sensors¶list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES | BLADE_GAP_GAMESA_ALLOWED_SENSOR_NAMES] | None, default:None) –List of the sensors to get the data for. The options are as shown below:
- GE: "Planetary", "LSS", "HSS", "Generator RS", "Generator GS", "Main Bearing", "Tower Sway Axial", "Tower Sway Transverse"
- Gamesa Vibration: "1 - Generator GS - Radial", "2 - Planetary - Axial", "3 - Main Bearing GS - Radial", "4 - HSS - Radial", "5 - Main Bearing RS - Axial", "6 - HSS - Axial", "7 - Generator RS - Axial", "8 - Generator RS - Radial"
- Gamesa Blade Gap: "Blade A", "Blade B", "Blade C"
These must be specified with the matching manufacturer and cannot be mixed. If GE is selected only GE sensors are allowed and vice versa.
By default None.
-
(acquisition_frequencies¶list[Literal['Low', 'High', 'Filter']] | None, default:None) –Acquisition frequency, only applicable for Gamesa turbines. By default, None
-
(variable_names¶list[Literal['Acceleration - X', 'Acceleration - Y', 'Position - X', 'Position - Y']] | None, default:None) –Names of the variables to filter by. Only applicable to Gamesa turbines when getting blade gap data (sensors must be from "Blade A", "Blade B", "Blade C"). By default, None
-
(filter_type¶Literal['and', 'or'], default:'and') –How to treat multiple filters. Can be one of ["and", "or"]. By default "and"
-
(output_type¶Literal['dict', 'DataFrame'], default:'DataFrame') –Output type of the data. Can be one of ["dict", "DataFrame"] By default "DataFrame"
Returns:
-
DataFrame–DataFrame with a MultiIndex[object_name, raw_data_name, timestamp] and columns: value, metadata. Value column contais a numpy ndarray with the Spectrum (2d array with first dimension as time and second as value).
-
dict[str, dict[str, dict[datetime, dict[str, dict[str, Any]]]]]–Dictionary in the format {object_name: {raw_data_name: {datetime: {value: value, metadata: metadata}, ...}, ...}, ...}
Source code in echo_postgres/vibration_spectrum.py
@validate_call
def get(
self,
period: DateTimeRange | list[date],
manufacturer: Literal["Gamesa", "GE"],
data_type: Literal["Vibration", "Blade Gap"] = "Vibration",
object_names: list[str] | None = None,
unit: Literal["Hz", "Order"] = "Order",
spectrum_type: Literal["Normal", "Envelope"] = "Normal",
amplitude_type: Literal["RMS", "Peak", "Peak-to-Peak"] = "Peak",
sensors: list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES | BLADE_GAP_GAMESA_ALLOWED_SENSOR_NAMES]
| None = None,
acquisition_frequencies: list[Literal["Low", "High", "Filter"]] | None = None,
variable_names: list[Literal["Acceleration - X", "Acceleration - Y", "Position - X", "Position - Y"]] | None = None,
filter_type: Literal["and", "or"] = "and",
output_type: Literal["dict", "DataFrame"] = "DataFrame",
) -> DataFrame | dict[str, dict[str, dict[datetime, dict[str, dict[str, Any]]]]]:
"""Gets the vibration Spectrum data.
Currently, this will process the data in two different ways depending on the manufacturer of the turbine:
- Gamesa: Time series data will be converted to spectrum using a FFT. If envelope is selected, the Hilbert transform will be used to get the envelope of the signal before the FFT.
- GE: Spectrum data will be used directly, converting the frequency axis to be relative to the generator shaft speed.
The values will be numpy ndarrays with two dimensions [2, :], where first dimension is the frequency (in hertz or orders) and the second is the value.
Assuming array is a value of one row and column "value", if you want to get frequency of the array, you can do array[0, :] and for the value itself array[1, :]
Parameters
----------
period : DateTimeRange | list[date]
Can be a DateTimeRange or a list of dates.
- If DateTimeRange, will get the data for the entire range (limiting on start and end)
- If list of dates, will get the data for each date in the list.
manufacturer : Literal["Gamesa", "GE"]
Manufacturer of the wind turbine. Either Gamesa or GE.
data_type : Literal["Vibration", "Blade Gap"], optional
Type of the data to get. Can be one of ["Vibration", "Blade Gap"]. If "Vibration", will get vibration data, if "Blade Gap", will get blade gap data. By default "Vibration"
object_names : list[str] | None, optional
Names of the objects to check. If None will check for all objects. By default None
unit : Literal["Hz", "Order"], optional
Unit of the frequency. Can be one of ["Hz", "Order"]. If Order is used, it will always be relative to the HSS/Generator Shaft speed.
By default "Order"
spectrum_type : Literal["Normal", "Envelope"], optional
What kind of spectrum should be returned. By default "Normal"
amplitude_type : Literal["RMS", "Peak", "Peak-to-Peak"], optional
Type of amplitude to return. Can be one of ["RMS", "Peak", "Peak-to-Peak"].
For GE turbines only Peak is allowed. Peak-to-Peak and RMS are not supported as the data is acquired directly as spectrum.
By default "Peak"
sensors : list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES | BLADE_GAP_GAMESA_ALLOWED_SENSOR_NAMES] | None
List of the sensors to get the data for. The options are as shown below:
- GE: "Planetary", "LSS", "HSS", "Generator RS", "Generator GS", "Main Bearing", "Tower Sway Axial", "Tower Sway Transverse"
- Gamesa Vibration: "1 - Generator GS - Radial", "2 - Planetary - Axial", "3 - Main Bearing GS - Radial", "4 - HSS - Radial", "5 - Main Bearing RS - Axial", "6 - HSS - Axial", "7 - Generator RS - Axial", "8 - Generator RS - Radial"
- Gamesa Blade Gap: "Blade A", "Blade B", "Blade C"
These must be specified with the matching manufacturer and cannot be mixed. If GE is selected only GE sensors are allowed and vice versa.
By default None.
acquisition_frequencies : list[Literal["Low", "High", "Filter"]] | None, optional
Acquisition frequency, only applicable for Gamesa turbines. By default, None
variable_names : list[Literal["Acceleration - X", "Acceleration - Y", "Position - X", "Position - Y"]] | None, optional
Names of the variables to filter by. Only applicable to Gamesa turbines when getting blade gap data (sensors must be from "Blade A", "Blade B", "Blade C"). By default, None
filter_type : Literal["and", "or"], optional
How to treat multiple filters. Can be one of ["and", "or"]. By default "and"
output_type : Literal["dict", "DataFrame"], optional
Output type of the data. Can be one of ["dict", "DataFrame"]
By default "DataFrame"
Returns
-------
DataFrame
DataFrame with a MultiIndex[object_name, raw_data_name, timestamp] and columns: value, metadata. Value column contais a numpy ndarray with the Spectrum (2d array with first dimension as time and second as value).
dict[str, dict[str, dict[datetime, dict[str, dict[str, Any]]]]]
Dictionary in the format {object_name: {raw_data_name: {datetime: {value: value, metadata: metadata}, ...}, ...}, ...}
"""
# checking arguments
if output_type not in ["dict", "DataFrame"]:
raise ValueError(f"output_type must be one of ['dict', 'DataFrame'], not {output_type}")
if unit not in ["Hz", "Order"]:
raise ValueError(f"unit must be one of Hz or Order, not {unit}")
_, wanted_names = self._check_get_args(
object_names=object_names,
period=period,
acquisition_frequencies=acquisition_frequencies,
variable_names=variable_names,
manufacturer=manufacturer,
data_type=data_type,
sensors=sensors,
spectrum_type=spectrum_type,
amplitude_type=amplitude_type,
filter_type=filter_type,
)
match manufacturer:
# * Gamesa -----------------------------------
case "Gamesa":
# getting time series
df: DataFrame = self._perfdb.vibration.timeseries.get(
period=period,
object_names=object_names,
manufacturer=manufacturer,
data_type=data_type,
sensors=sensors,
acquisition_frequencies=acquisition_frequencies,
variable_names=variable_names,
filter_type=filter_type,
output_type="DataFrame",
)
if not df.empty:
# removing time component from time series
df["value"] = df["value"].apply(lambda x: x[1, :])
match spectrum_type:
# * Normal ---------------------------------
case "Normal":
# no need for further processing
pass
# * Envelope ---------------------------------
case "Envelope":
# performing hilbert transform to get envelope
df["value"] = df["value"].apply(lambda x: np.abs(hilbert(x)))
case _:
raise ValueError(f"Spectrum type {spectrum_type} is not valid")
# getting number of samples in value
df["n_samples"] = df["value"].apply(lambda x: x.size)
# applying fft
df["value"] = df["value"].apply(lambda x: np.abs(rfft(x, workers=4)))
# normalize by number of samples
# this is needed to get the correct amplitude as the fft is not normalized. See https://numpy.org/doc/stable/reference/routines.fft.html#module-numpy.fft for more details
df["value"] = df[["value", "n_samples"]].apply(lambda x: x["value"] / x["n_samples"], axis=1)
# double the magnitude to account for negative frequencies. DC component is not doubled
# this is already the peak value
df["value"] = df["value"].apply(lambda x: np.concatenate(([x[0]], x[1:] * 2)))
# converting to Peak-to-Peak or RMS if necessary
match amplitude_type:
case "Peak":
# already peak
pass
case "Peak-to-Peak":
# doubling the value, except for DC component
df["value"] = df["value"].apply(lambda x: np.concatenate(([x[0]], x[1:] * 2)))
case "RMS":
# converting to RMS by dividing by sqrt(2), except for DC component
df["value"] = df["value"].apply(lambda x: np.concatenate(([x[0]], x[1:] / np.sqrt(2))))
case _:
raise ValueError(f"Amplitude type {amplitude_type} is not valid")
# getting frequencies
df["frequencies"] = df[["n_samples", "sampling_time"]].apply(
lambda row: rfftfreq(n=int(row["n_samples"]), d=row["sampling_time"]),
axis=1,
)
# converting to orders in case necessary
if unit == "Order":
# raise error in case we are getting blade gap data, as it doesn't have generator speed
if data_type == "Blade Gap":
raise ValueError(
"Unit 'Order' is not supported for Blade Gap data type. Please use 'Hz' instead.",
)
df["frequencies"] = df[["frequencies", "metadata"]].apply(
lambda row: row["frequencies"] / (row["metadata"]["generator_speed_rpm"] / 60),
axis=1,
)
# converting back to only one array with two dimensions in value column
df["value"] = df[["value", "frequencies"]].apply(
lambda row: np.concatenate(
(
np.expand_dims(row["frequencies"], axis=0),
np.expand_dims(row["value"], axis=0),
),
axis=0,
),
axis=1,
)
# dropping unwanted columns
df = df.drop(columns=["n_samples", "frequencies"], errors="ignore")
# * GE ---------------------------------------
case "GE":
# checking which component types we need to get for the turbines
if sensors is None:
sensors = list(VIBRATION_CONFIG[manufacturer]["sensors"][data_type].keys())
required_component_types = list(
{VIBRATION_CONFIG[manufacturer]["sensors"][data_type][sensor]["ComponentType"] for sensor in sensors},
)
# adding Gearbox if it's not present - this is required as the gearbox holds the attributes with ratio between shafts
if "Gearbox" not in required_component_types:
required_component_types.append("Gearbox")
required_component_types.sort()
# getting the component instances for the wanted turbines and component types
component_instances: DataFrame = self._perfdb.components.instances.history.get(
object_names=object_names,
component_types=required_component_types,
period=period
if isinstance(period, DateTimeRange)
else None, # only using period if it's a DateTimeRange, if its a list of dates we just get all
get_attributes=True,
)
# validating if all the required attributes are present
self._validate_ge_component_attributes(components_df=component_instances)
# getting raw data
df: DataFrame = self._perfdb.rawdata.values.get(
object_names=wanted_names["object_names"],
raw_data_names=wanted_names["raw_data_names"],
period=period,
filter_type=filter_type,
)
# converting names
df = self._convert_raw_names_to_sensor_names(
df=df,
manufacturer=manufacturer,
spectrum_type=spectrum_type,
)
# converting from orders relative to it's reference shaft to orders relative to the generator shaft
df = self._convert_ge_freq_orders(spectrum_df=df, components_df=component_instances, spectrum_type=spectrum_type, unit=unit)
# converting back to only one array with two dimensions in value column
df["value"] = df[["value", "frequencies"]].apply(
lambda row: np.concatenate(
(
np.expand_dims(row["frequencies"], axis=0),
np.expand_dims(row["value"], axis=0),
),
axis=0,
),
axis=1,
)
# dropping unwanted columns
df = df.drop(columns=["frequencies"])
case _:
raise ValueError(f"Manufacturer {manufacturer} is not valid")
if output_type == "DataFrame":
return df
# converting to dict
result = df.to_dict(orient="index")
final_result = {}
for (object_name, sensor, acquisition_frequency, timestamp), values in result.items():
if object_name not in final_result:
final_result[object_name] = {}
if sensor not in final_result[object_name]:
final_result[object_name][sensor] = {}
if acquisition_frequency not in final_result[object_name][sensor]:
final_result[object_name][sensor][acquisition_frequency] = {}
final_result[object_name][sensor][acquisition_frequency][timestamp] = values
return final_result
get_timestamps(manufacturer, data_type='Vibration', object_names=None, period=None, spectrum_type='Normal', sensors=None, acquisition_frequencies=None, variable_names=None, value_type='date', output_type='DataFrame')
¶
Gets the timestamps/dates where there is vibration spectrum data available.
If you only want the timestamps as a list, you can set the output_type to 'DataFrame' and then do df["timestamp"].unique().tolist() at the result.
Parameters:
-
(manufacturer¶Literal['Gamesa', 'GE']) –Manufacturer of the wind turbine. Either Gamesa or GE.
-
(data_type¶Literal['Vibration', 'Blade Gap'], default:'Vibration') –Type of the data to get. Can be one of ["Vibration", "Blade Gap"]. If "Vibration", will get vibration data, if "Blade Gap", will get blade gap data.
-
(object_names¶list[str] | None, default:None) –Names of the objects to check. If None will check for all objects. By default None
-
(period¶DateTimeRange | None, default:None) –Period to check. If None the entire raw_data_values table will be scanned. By default None
-
(spectrum_type¶Literal['Normal', 'Envelope'], default:'Normal') –What kind of spectrum should be returned. By default, Normal
-
(sensors¶list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES] | None, default:None) –List of the sensors to get the data for. The options are as shown below:
- GE: "Planetary", "LSS", "HSS", "Generator RS", "Generator GS", "Main Bearing", "Tower Sway Axial", "Tower Sway Transverse"
- Gamesa: "1 - Generator GS - Radial", "2 - Planetary - Axial", "3 - Main Bearing GS - Radial", "4 - HSS - Radial", "5 - Main Bearing RS - Axial", "6 - HSS - Axial", "7 - Generator RS - Axial", "8 - Generator RS - Radial"
These must be specified with the matching manufacturer and cannot be mixed. If GE is selected only GE sensors are allowed and vice versa.
By default None.
-
(acquisition_frequencies¶list[Literal['Low', 'High', 'Filter']] | None, default:None) –Acquisition frequency, only applicable for Gamesa turbines. By default, None
-
(variable_names¶list[Literal['Acceleration - X', 'Acceleration - Y', 'Position - X', 'Position - Y']] | None, default:None) –Names of the variables to filter by. Only applicable to Gamesa turbines when getting blade gap data (sensors must be from "Blade A", "Blade B", "Blade C"). By default, None
-
(value_type¶Literal['timestamp', 'date'], default:'date') –If timestamp, will return timestamps as datetimes, if date will return as date (removing hour, minute, second).
-
(output_type¶Literal['dict', 'DataFrame'], default:'DataFrame') –Output type of the data. Can be one of ["dict", "DataFrame"] By default "DataFrame"
Returns:
-
DataFrame–DataFrame with columns: object_name, sensor, acquisition_frequency, timestamp. Index can be ignored.
-
dict[str, dict[str, list[date | datetime]]]–Dictionary in the format {object_name: {sensor: {acquisition_frequency: [date | datetime], ...}, ...}, ...}
Source code in echo_postgres/vibration_spectrum.py
@validate_call
def get_timestamps(
self,
manufacturer: Literal["Gamesa", "GE"],
data_type: Literal["Vibration", "Blade Gap"] = "Vibration",
object_names: list[str] | None = None,
period: DateTimeRange | None = None,
spectrum_type: Literal["Normal", "Envelope"] = "Normal",
sensors: list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES | BLADE_GAP_GAMESA_ALLOWED_SENSOR_NAMES]
| None = None,
acquisition_frequencies: list[Literal["Low", "High", "Filter"]] | None = None,
variable_names: list[Literal["Acceleration - X", "Acceleration - Y", "Position - X", "Position - Y"]] | None = None,
value_type: Literal["timestamp", "date"] = "date",
output_type: Literal["dict", "DataFrame"] = "DataFrame",
) -> DataFrame | dict[str, dict[str, list[date | datetime]]]:
"""Gets the timestamps/dates where there is vibration spectrum data available.
If you only want the timestamps as a list, you can set the output_type to 'DataFrame' and then do df["timestamp"].unique().tolist() at the result.
Parameters
----------
manufacturer : Literal["Gamesa", "GE"]
Manufacturer of the wind turbine. Either Gamesa or GE.
data_type : Literal["Vibration", "Blade Gap"], optional
Type of the data to get. Can be one of ["Vibration", "Blade Gap"]. If "Vibration", will get vibration data, if "Blade Gap", will get blade gap data.
object_names : list[str] | None, optional
Names of the objects to check. If None will check for all objects. By default None
period : DateTimeRange | None, optional
Period to check. If None the entire raw_data_values table will be scanned. By default None
spectrum_type : Literal["Normal", "Envelope"], optional
What kind of spectrum should be returned. By default, Normal
sensors : list[VIBRATION_GE_ALLOWED_SENSOR_NAMES | VIBRATION_GAMESA_ALLOWED_SENSOR_NAMES] | None
List of the sensors to get the data for. The options are as shown below:
- GE: "Planetary", "LSS", "HSS", "Generator RS", "Generator GS", "Main Bearing", "Tower Sway Axial", "Tower Sway Transverse"
- Gamesa: "1 - Generator GS - Radial", "2 - Planetary - Axial", "3 - Main Bearing GS - Radial", "4 - HSS - Radial", "5 - Main Bearing RS - Axial", "6 - HSS - Axial", "7 - Generator RS - Axial", "8 - Generator RS - Radial"
These must be specified with the matching manufacturer and cannot be mixed. If GE is selected only GE sensors are allowed and vice versa.
By default None.
acquisition_frequencies : list[Literal["Low", "High", "Filter"]] | None, optional
Acquisition frequency, only applicable for Gamesa turbines. By default, None
variable_names : list[Literal["Acceleration - X", "Acceleration - Y", "Position - X", "Position - Y"]] | None, optional
Names of the variables to filter by. Only applicable to Gamesa turbines when getting blade gap data (sensors must be from "Blade A", "Blade B", "Blade C"). By default, None
value_type : Literal["timestamp", "date"], optional
If timestamp, will return timestamps as datetimes, if date will return as date (removing hour, minute, second).
output_type : Literal["dict", "DataFrame"], optional
Output type of the data. Can be one of ["dict", "DataFrame"]
By default "DataFrame"
Returns
-------
DataFrame
DataFrame with columns: object_name, sensor, acquisition_frequency, timestamp. Index can be ignored.
dict[str, dict[str, list[date | datetime]]]
Dictionary in the format {object_name: {sensor: {acquisition_frequency: [date | datetime], ...}, ...}, ...}
"""
# checking arguments
if output_type not in ["dict", "DataFrame"]:
raise ValueError(f"output_type must be one of ['dict', 'DataFrame'], not {output_type}")
if value_type not in ["date", "timestamp"]:
raise ValueError(f"value_type must be one of ['date', 'timestamp']. Got {value_type}")
where, _ = self._check_get_args(
object_names=object_names,
period=period,
acquisition_frequencies=acquisition_frequencies,
data_type=data_type,
variable_names=variable_names,
manufacturer=manufacturer,
sensors=sensors,
spectrum_type=spectrum_type,
amplitude_type=None,
filter_type="and",
)
# building the query
query = [
sql.SQL("SELECT DISTINCT object_name, raw_data_name, timestamp{type_cast} FROM v_raw_data_values ").format(
type_cast=sql.SQL("::DATE") if value_type == "date" else sql.SQL("::TIMESTAMP"),
),
where,
sql.SQL(" ORDER BY object_name, raw_data_name, timestamp"),
]
query = sql.Composed(query)
# executing the query
with self._perfdb.conn.reconnect() as conn:
df: DataFrame = conn.read_to_pandas(
query,
dtype={
"object_name": "string[pyarrow]",
"raw_data_name": "string[pyarrow]",
"timestamp": "datetime64[s]",
},
)
# converting names
df = df.set_index(["object_name", "raw_data_name"])
df = self._perfdb.vibration.spectrum._convert_raw_names_to_sensor_names( # noqa: SLF001
df=df,
manufacturer=manufacturer,
spectrum_type=spectrum_type,
)
df = df.reset_index(drop=False)
if output_type == "DataFrame":
return df
# converting to dict
df = df.groupby(["object_name", "sensor", "acquisition_frequency"])["timestamp"].apply(list)
result = df.to_dict()
final_result = {}
for (object_name, sensor, acquisition_frequency), value in result.items():
if object_name not in final_result:
final_result[object_name] = {}
if sensor not in final_result[object_name]:
final_result[object_name][sensor] = {}
final_result[object_name][sensor][acquisition_frequency] = value
return final_result