392 lines
16 KiB
Python
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
|
|
|