Combined-Sensor-Uncertainty/code/inputs/Inputs.py
SchrodingerError 3c59598d38 Initial Commit
2024-08-14 14:40:12 -05:00

392 lines
16 KiB
Python

from random import gauss
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from inputs.Pressure_Sensors import PressureSensor
from inputs.Temperature_Sensors import TemperatureSensor
from inputs.Force_Sensors import ForceSensor
class Input():
def __init__(self, true_value: float, input_errors: dict = dict(), name: str = None):
self.true_value = true_value
self.input_errors = input_errors
self.name = name if name is not None else type(self).__name__
self.parse_input_errors()
self.error_gain: float = 1.0
# Values to be used for data analysis at the end of the Monte Carlo
self.all_readings: list[float] = []
self.overall_average: float = 0.0
self.overall_uncertainty: float = 0.0
def __add__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'add')
def __sub__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'sub')
def __mul__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'mul')
def __truediv__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'div')
def __radd__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'add')
def __rsub__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'sub')
def __rmul__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'mul')
def __rtruediv__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'div')
def __pow__(self, exponent) -> 'CombinedInput':
return CombinedInput(self, exponent, 'pow')
def __rpow__(self, other)-> 'CombinedInput':
return CombinedInput(other, self, 'pow')
def get_name(self) -> str:
return self.name
def get_input_errors(self) -> str:
return self.name + "\n" + str(self.input_errors)
def get_arithmetic(self) -> str:
return "{" + self.get_name() + "}"
def set_error_gain(self, new_gain: float) -> None:
self.error_gain = new_gain
def parse_input_errors(self) -> None:
self.k = self.input_errors.get("k", 2) # By default, assume each uncertainty is given as 2 stdev
self.input_errors["k"] = self.k
# Static error band
self.fso = self.input_errors.get("fso", 0.0)
self.input_errors["fso"] = self.fso
self.static_error_stdev = self.fso * self.input_errors.get("static_error_band", 0.0) / 100 / self.k
self.input_errors["static_error_stdev"] = self.static_error_stdev
# Repeatability error
self.repeatability_error_stdev = self.fso * self.input_errors.get("repeatability", 0.0) / 100 / self.k
self.input_errors["repeatability_error_stdev"] = self.repeatability_error_stdev
# Hysteresis error
self.hysteresis_error_stdev = self.fso * self.input_errors.get("hysteresis", 0.0) / 100 / self.k
self.input_errors["hysteresis_error_stdev"] = self.hysteresis_error_stdev
# Non-linearity
self.nonlinearity_error_stdev = self.fso * self.input_errors.get("non-linearity", 0.0) / 100 / self.k
self.input_errors["non-linearity_error_stdev"] = self.nonlinearity_error_stdev
# Thermal error
self.temperature_offset = self.input_errors.get("temperature_offset", 0.0)
self.input_errors["temperature_offset"] = self.temperature_offset
self.thermal_error_offset = self.fso * self.temperature_offset * self.input_errors.get("temperature_zero_error", 0.0) / 100
self.input_errors["thermal_error_offset"] = self.thermal_error_offset
def calc_static_error(self) -> float:
return gauss(0, self.static_error_stdev)
def calc_repeatability_error(self) -> float:
return gauss(0, self.repeatability_error_stdev)
def calc_hysteresis_error(self) -> float:
return gauss(0, self.hysteresis_error_stdev)
def calc_nonlinearity_error(self) -> float:
return gauss(0, self.nonlinearity_error_stdev)
def calc_thermal_zero_error(self) -> float:
return self.thermal_error_offset
def calc_error(self) -> float:
static_error = self.calc_static_error()
repeatability_error = self.calc_repeatability_error()
hysteresis_error = self.calc_hysteresis_error()
nonlinearity_error = self.calc_nonlinearity_error()
thermal_zero_error = self.calc_thermal_zero_error()
return (static_error + repeatability_error + hysteresis_error + nonlinearity_error + thermal_zero_error) * self.error_gain
def get_reading(self) -> float:
'''Apply the pre-specified error and return a realistic value to mimic the reading of a sensor with error.'''
error: float = self.calc_error()
noisy_value: float = self.true_value + error
# Append the final value to self.all_readings for final calculations at the end
self.all_readings.append(noisy_value)
return noisy_value
def get_reading_isolating_input(self, input_to_isolate: 'Input'):
'''Gets true value from input except from the input to isolate'''
if self == input_to_isolate:
return self.get_reading()
else:
return self.get_true()
def get_true(self) -> float:
return self.true_value
def set_true(self, new_true: float) -> None:
self.true_value = new_true
self.parse_input_errors()
def reset_error_gain(self) -> None:
self.set_error_gain(1.0)
class CombinedInput(Input):
def __init__(self, sensor1, sensor2, operation):
self.sensor1: 'Input' = sensor1
self.sensor2: 'Input' = sensor2
self.operation = operation
def get_input_errors(self) -> str:
string = ""
if isinstance(self.sensor1, Input):
string += f"{self.sensor1.get_input_errors()}\n"
if isinstance(self.sensor2, Input):
string += f"{self.sensor2.get_input_errors()}\n"
return string
def get_arithmetic(self) -> str:
operation_char = ""
match self.operation:
case "add":
operation_char = "+"
case "sub":
operation_char = "-"
case "mul":
operation_char = "*"
case "div":
operation_char = "/"
case "pow":
operation_char = "**"
string = f"({self.sensor1.get_arithmetic() if isinstance(self.sensor1, Input) else str(self.sensor1)} "
string += operation_char
string += f" {self.sensor2.get_arithmetic() if isinstance(self.sensor2, Input) else str(self.sensor2)})"
return string
def get_reading(self) -> float:
reading1 = self.sensor1.get_reading() if isinstance(self.sensor1, Input) or isinstance(self.sensor1, CombinedInput) else self.sensor1
reading2 = self.sensor2.get_reading() if isinstance(self.sensor2, Input) or isinstance(self.sensor2, CombinedInput) else self.sensor2
# No need to add error as the error already comes from the sensor.get_reading()
if self.operation == 'add':
return reading1 + reading2
elif self.operation == 'sub':
return reading1 - reading2
elif self.operation == 'mul':
return reading1 * reading2
elif self.operation == 'div':
if reading2 == 0:
raise ZeroDivisionError("Division by zero is undefined.")
return reading1 / reading2
elif self.operation == 'pow':
return reading1 ** reading2
else:
raise ValueError(f"Unsupported operation: {self.operation}")
def get_reading_isolating_input(self, input_to_isolate: 'Input'):
'''Gets true value from every input except the input to isolate'''
reading1 = self.sensor1.get_reading_isolating_input(input_to_isolate) if isinstance(self.sensor1, Input) or isinstance(self.sensor1, CombinedInput) else self.sensor1
reading2 = self.sensor2.get_reading_isolating_input(input_to_isolate) if isinstance(self.sensor2, Input) or isinstance(self.sensor2, CombinedInput) else self.sensor2
if self.operation == 'add':
return reading1 + reading2
elif self.operation == 'sub':
return reading1 - reading2
elif self.operation == 'mul':
return reading1 * reading2
elif self.operation == 'div':
if reading2 == 0:
raise ZeroDivisionError("Division by zero is undefined.")
return reading1 / reading2
elif self.operation == 'pow':
return reading1 ** reading2
else:
raise ValueError(f"Unsupported operation: {self.operation}")
def get_true(self) -> float:
reading1 = self.sensor1.get_true() if isinstance(self.sensor1, Input) or isinstance(self.sensor1, CombinedInput) else self.sensor1
reading2 = self.sensor2.get_true() if isinstance(self.sensor2, Input) or isinstance(self.sensor2, CombinedInput) else self.sensor2
# No need to add error as the error already comes from the sensor.get_reading()
if self.operation == 'add':
return reading1 + reading2
elif self.operation == 'sub':
return reading1 - reading2
elif self.operation == 'mul':
return reading1 * reading2
elif self.operation == 'div':
if reading2 == 0:
raise ZeroDivisionError("Division by zero is undefined.")
return reading1 / reading2
elif self.operation == 'pow':
return reading1 ** reading2
else:
raise ValueError(f"Unsupported operation: {self.operation}")
def reset_error_gain(self) -> None:
'''Resets the error gain of all connected inputs to 1.0'''
if isinstance(self.sensor1, Input):
self.sensor1.reset_error_gain()
if isinstance(self.sensor2, Input):
self.sensor2.reset_error_gain()
def __add__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'add')
def __sub__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'sub')
def __mul__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'mul')
def __truediv__(self, other) -> 'CombinedInput':
return CombinedInput(self, other, 'div')
def __pow__(self, other)-> 'CombinedInput':
return CombinedInput(self, other, 'pow')
def __radd__(self, other)-> 'CombinedInput':
return CombinedInput(other, self, 'add')
def __rsub__(self, other)-> 'CombinedInput':
return CombinedInput(other, self, 'sub')
def __rmul__(self, other)-> 'CombinedInput':
return CombinedInput(other, self, 'mul')
def __rtruediv__(self, other)-> 'CombinedInput':
return CombinedInput(other, self, 'div')
def __rpow__(self, other)-> 'CombinedInput':
return CombinedInput(other, self, 'pow')
class AveragingInput(Input):
def __init__(self, sensors: list['Input']):
if not sensors:
raise ValueError("The list of sensors cannot be empty")
# Ensure all sensors are of the same parent type
parent_class = type(sensors[0])
while not issubclass(parent_class, Input):
parent_class = parent_class.__bases__[0]
super().__init__(true_value=0.0, input_errors={})
self.sensors = sensors
def get_first(self) -> 'Input':
return self.sensors[0]
def get_input_errors(self) -> str:
string = ""
for sensor in self.sensors:
if isinstance(sensor, Input):
string += f"{sensor.get_input_errors()}\n\n"
return string
def get_arithmetic(self) -> str:
string = f"Average({self.sensors[0].get_arithmetic()}"
if len(self.sensors) != 1:
for sensor in self.sensors[1:]:
string += ", "
string += sensor.get_arithmetic()
return string + ")"
def get_reading(self) -> float:
total = sum(sensor.get_reading() for sensor in self.sensors)
average = total / len(self.sensors)
# No need to add error as the error already comes from the sensor.get_reading()
# Append the final value to self.all_readings for final calculations at the end
self.all_readings.append(average)
return average
def get_reading_isolating_input(self, input_to_isolate: 'Input'):
'''Gets true value from input except from the input to isolate'''
total = sum(sensor.get_reading_isolating_input(input_to_isolate) for sensor in self.sensors)
average = total / len(self.sensors)
# No need to add error as the error already comes from the sensor.get_reading()
# Append the final value to self.all_readings for final calculations at the end
self.all_readings.append(average)
return average
def get_true(self) -> float:
total = sum(sensor.get_true() for sensor in self.sensors)
average = total / len(self.sensors)
# Append the final value to self.all_readings for final calculations at the end
self.all_readings.append(average)
return average
def reset_error_gain(self) -> None:
sensor: Input = None
for sensor in self.sensors:
sensor.reset_error_gain()
class PhysicalInput(Input):
'''A numerical input with an associated tolerance (or uncertainty in %). An example is the diameter of an orifice.
Typically, tolerance represents 3 standard deviations. So, an orifice of 5mm with tolerance of 1mm
would fall within [4, 6] 99.73% of the time.'''
def __init__(self, true_value: float, input_errors: dict = dict(), name: str = None):
super().__init__(true_value, input_errors=input_errors, name=name)
def parse_input_errors(self) -> None:
self.k = self.input_errors.get("k", 3) # By default, assume each uncertainty is given as 3 stdev
self.input_errors["k"] = self.k
self.tolerance:float = self.input_errors.get("tolerance", None)
self.input_errors["tolerance"] = self.tolerance
self.uncertainty:float = self.input_errors.get("uncertainty", None)
self.input_errors["uncertainty"] = self.uncertainty
if self.tolerance is not None:
self.std_dev: float = self.tolerance / self.k # Because tolerance represents 3 stdev
else:
self.std_dev = self.true_value * self.uncertainty / 100
self.input_errors["std_dev"] = self.std_dev
# Handle any temperature contract or expansion
# Set the self.true_value to a new value after contract
# Set self.measured_value is what we will apply an error to
self.measured_value = self.true_value
if "temperature_offset" in self.input_errors:
self.cte = self.input_errors.get("cte", 8.8E-6)
self.temperature_offset = self.input_errors.get("temperature_offset", 0)
expansion_amount = self.measured_value * self.cte * self.temperature_offset
self.true_value = self.measured_value + expansion_amount
def get_arithmetic(self) -> str:
return "{" + str(self.name) + "}"
def calc_error(self) -> float:
return gauss(0, self.std_dev)
def get_reading(self) -> float:
'''Apply the pre-specified error and return a realistic value to mimic the reading of a sensor with error.'''
error: float = self.calc_error()
noisy_value: float = self.measured_value + error
# Append the final value to self.all_readings for final calculations at the end
self.all_readings.append(noisy_value)
return noisy_value