import numpy as np from .Ordinate import Ordinate from .Absolute import Absolute from .Angle import to_dms from .MeasurementType import MeasurementType as mt from pydantic import BaseModel class Calculate(BaseModel): """ Class object for performing calculations. Contains the following: angle: average angle across a measurement type residual: average residual across a measurement type hs: Multiplier for inclination claculations. +1 if measurment was taken in northern hemisphere, -1 if measurement was taken in the southern hemishpere. ordinate: Variometer data. Ordinate object(contains a datapoint for H, E, Z, and F) ud: Multiplier for inclination calculations. +1 if instrument is oriented upward, -1 if instrument if oriented downward. shift: Degree shift in inclination measurements. """ angle: float = None residual: float = None ordinate: Ordinate = None f: float = None ud: int = None pm: int = None shift: int = None def calculate(reading): """ Calculate/recalculate absolute from a Reading object's ordinates, measurements, and metadata. Outputs a list of absolutes containing baseline, absolute, and element name. Also reutrns the scale value. """ # gather oridinates, measuremenets, and metadata objects from reading metadata = reading.metadata ordinates = reading.ordinates ordinate_index = reading.ordinate_index() measurements = reading.measurements measurement_index = reading.measurement_index() # define measurement types used to calculate inclination inclination_types = [mt.NORTH_DOWN, mt.NORTH_UP, mt.SOUTH_DOWN, mt.SOUTH_UP] # get ordinate values across h, e, z, and f for inclination measurement types inclination_ordinates = [ o for o in ordinates if o.measurement_type in inclination_types ] # get average ordinate values across h, e, z, and f mean = average_ordinate(inclination_ordinates, None) # calculate inclination inclination, f = calculate_I( measurement_index, inclination_ordinates, ordinate_index, mean, metadata, ) # calculate absolutes h_abs, z_abs = calculate_absolutes(f, inclination) # calculate baselines for H and Z h_b, z_b = calculate_baselines(h_abs, z_abs, mean, reading.pier_correction) # calculate scale value scale_ordinates = ordinate_index[mt.NORTH_DOWN_SCALE] scale_measurements = measurement_index[mt.NORTH_DOWN_SCALE] scale = calculate_scale(f, scale_ordinates, scale_measurements, inclination,) # calculate declination absolute and baseline d_b, d_abs = calculate_D( ordinate_index, measurements, measurement_index, metadata["mark_azimuth"], h_b, ) # return results as a set of Absolute objects along with the calculated scale value resultH = Absolute(element="H", baseline=h_b, absolute=h_abs) resultD = Absolute(element="D", baseline=d_b, absolute=d_abs) resultZ = Absolute(element="Z", baseline=z_b, absolute=z_abs) result = [resultH, resultD, resultZ] return result, scale def calculate_I(measurements, ordinates, ordinates_index, mean, metadata): """ Calculate inclination angles from measurements, ordinates, average ordinates from every measurement, and metadata. Returns inclination angle and calculated average f """ # get first inclination angle, assumed to be the southdown angle Iprime = average_angle(measurements, mt.SOUTH_DOWN) if Iprime >= 100: Iprime -= 200 # get multiplier for hempisphere the observatory is located in # 1 if observatory is in northern hemisphere # -1 if observatory is in southern hemisphere hs = metadata["hemisphere"] # gather calculation objects for each measurement type southdown = Calculate( shift=-200, ud=1, pm=1, angle=average_angle(measurements, mt.SOUTH_DOWN), residual=average_residual(measurements, mt.SOUTH_DOWN), ordinate=average_ordinate(ordinates_index, mt.SOUTH_DOWN), ) southup = Calculate( shift=200, ud=-1, pm=-1, angle=average_angle(measurements, mt.SOUTH_UP), residual=average_residual(measurements, mt.SOUTH_UP), ordinate=average_ordinate(ordinates_index, mt.SOUTH_UP), ) northup = Calculate( shift=0, ud=-1, pm=1, angle=average_angle(measurements, mt.NORTH_UP), residual=average_residual(measurements, mt.NORTH_UP), ordinate=average_ordinate(ordinates_index, mt.NORTH_UP), ) northdown = Calculate( shift=400, ud=1, pm=-1, angle=average_angle(measurements, mt.NORTH_DOWN), residual=average_residual(measurements, mt.NORTH_DOWN), ordinate=average_ordinate(ordinates_index, mt.NORTH_DOWN), ) # set inclination value for looping = Iprime inclination = Iprime # add one to inclination value to enter the loop Inclination = inclination + 1 # loop condition while abs(Inclination - inclination) > 0.0001: # set temporary inclination variable to hold previous step's inclination Inclination = inclination # calculate f for inclination measurement types southdown.f = calculate_f(southdown.ordinate, mean, inclination) northdown.f = calculate_f(northdown.ordinate, mean, inclination) southup.f = calculate_f(southup.ordinate, mean, inclination) northup.f = calculate_f(northup.ordinate, mean, inclination) # gather measurements into array(necessary because f were recalculated) measurements = [southup, southdown, northup, northdown] # average f values for inclination measurement types f = np.average([i.f for i in measurements]) # calculation inclination for each inclination measurement type and average inclination = np.average( [calculate_measurement_inclination(m, hs) for m in measurements] ) # loop exits once the difference in average inclination between steps is lower than 0.0001 return inclination, f def calculate_D(ordinates_index, measurements, measurements_index, azimuth, h_b): """ Calculate declination absolute and declination baseline from ordinates, measurements, measurement_index(dictionary), azimuth and H baseline Returns absolute and baseline for declination. """ # specify mark measurement types mark_types = [ mt.FIRST_MARK_DOWN, mt.FIRST_MARK_UP, mt.SECOND_MARK_DOWN, mt.SECOND_MARK_UP, ] # average mark angles # note that angles are being converted to geon average_mark = np.average( [ convert_to_geon(m.angle) for m in measurements if m.measurement_type in mark_types ] ) # add 100 if mark up is greater than mark down # subtract 100 otherwise if ( measurements_index["FirstMarkUp"][0].angle < measurements_index["FirstMarkDown"][0].angle ): average_mark += 100 else: average_mark -= 100 # gather calculation objects for each declination measurement type # note that the pm(plus minus) multiplier has been repurposed. # West facing measurements have a multiplier of -1 # East facing measurements have a multipllier of 1 westdown = Calculate( angle=average_angle(measurements_index, mt.WEST_DOWN), residual=average_residual(measurements_index, mt.WEST_DOWN), ordinate=average_ordinate(ordinates_index, mt.WEST_DOWN), pm=-1, ) westup = Calculate( angle=average_angle(measurements_index, mt.WEST_UP), residual=average_residual(measurements_index, mt.WEST_UP), ordinate=average_ordinate(ordinates_index, mt.WEST_UP), pm=-1, ) eastdown = Calculate( angle=average_angle(measurements_index, mt.EAST_DOWN), residual=average_residual(measurements_index, mt.EAST_DOWN), ordinate=average_ordinate(ordinates_index, mt.EAST_DOWN), pm=1, ) eastup = Calculate( angle=average_angle(measurements_index, mt.EAST_UP), residual=average_residual(measurements_index, mt.EAST_UP), ordinate=average_ordinate(ordinates_index, mt.EAST_UP), pm=1, ) # convert azimuth to geon azimuth = (int(azimuth / 100) + (azimuth % 100) / 60) / 0.9 # gather declination measurements into array measurements = [westdown, westup, eastdown, eastup] # average meridian terms calculated from each declination measurements meridian = np.average([calculate_meridian_term(i, h_b) for i in measurements]) # add subtract average mark angle from average meridian angle and add # azimuth(in geon) to get the declination baseline D_b = (meridian - average_mark) + azimuth # convert decimal baseline into dms baseline d_b = round(D_b * 54, 2) d_b_dms = int(d_b / 60) * 100 + ((d_b / 60) % 1) * 60 # gather first declination measurements' H ans E ordinates wd_E_1 = ordinates_index[mt.WEST_DOWN][0].e wd_H_1 = ordinates_index[mt.WEST_DOWN][0].h # calculate Declination baseline d_abs = D_b + np.arctan(wd_E_1 / (h_b + wd_H_1)) * (200 / np.pi) d_abs = round(d_abs * 54, 1) # convert decimal absolute into dms absolute d_abs_dms = int(d_abs / 60) * 100 + ((d_abs / 60) % 1) * 60 return d_b_dms, d_abs_dms def calculate_absolutes(f, inclination): """ Calculate absolutes for H, Z and F from computed average f value(from inclination computations) and calculated inclination angle. Returns baselines for H and Z """ # convert inclination to radians i = (np.pi / 200) * (inclination) h_abs = f * np.cos(i) z_abs = f * np.sin(i) return h_abs, z_abs def calculate_baselines(h_abs, z_abs, mean, pier_correction): """ Calculate baselines with H and Z absolutes, average ordinates across all measurements, and pier correction(metadata). Returns H and Z baselines """ correction = pier_correction / 5 h_b = round(np.sqrt(h_abs ** 2 - mean.e ** 2) - mean.h, 1) - correction z_b = round(z_abs - mean.z, 1) - correction return h_b, z_b def calculate_scale(f, ordinates, measurements, inclination): """ Calculate scale value from calulated f(from inclination computations), inclination, and the measurements/ordinates taken for scaling purposes. Such measurements usually present themselves as a set of three North Down measurements, where the final two measuremets(and ordinates) are used for scaling. """ first_ord = ordinates[0] second_ord = ordinates[1] first_measurement = measurements[0] second_measurement = measurements[1] field_change = ( ( -np.sin(inclination * np.pi / 200) * (second_ord.h - first_ord.h) / f + np.cos(inclination * np.pi / 200) * (second_ord.z - first_ord.z) / f ) * 200 / np.pi ) field_change += 0.1852 residual_change = abs(second_measurement.residual - first_measurement.residual) scale_value = (f * field_change / residual_change) * np.pi / 200 return scale_value def average_angle(measurements, type): """ Compute average angle from a dictionary of measurements and specified measurement type. """ return np.average([convert_to_geon(m.angle) for m in measurements[type]]) def average_residual(measurements, type): """ Compute average residual from a dictionary of measurements and specified measurement type. """ return np.average([m.residual for m in measurements[type]]) def average_ordinate(ordinates, type): """ Compute average ordinate from a dictionary of ordinates and specified measurement type. """ if type is not None: ordinates = ordinates[type] o = Ordinate(measurement_type=type) avgs = np.average([[o.h, o.e, o.z, o.f] for o in ordinates], axis=0,) o.h, o.e, o.z, o.f = avgs return o def calculate_f(ordinate, mean, inclination): """ calculate f for a measurement type using a measurement's average ordinates, average ordinate across all measurements, and calculated inclination. """ # get channel means form all ordinates # calculate f using current step's inclination angle f = ( mean.f + (ordinate.h - mean.h) * np.cos(inclination * np.pi / 200) + (ordinate.z - mean.z) * np.sin(inclination * np.pi / 200) + ((ordinate.e) ** 2 - (mean.e) ** 2) / (2 * mean.f) ) return f def calculate_measurement_inclination(calculation, hs): """ Calculate a measurement's inclination value using Calculate items' elements. """ return calculation.shift + calculation.pm * ( +calculation.angle + calculation.ud * (hs * np.arcsin(calculation.residual / calculation.f) * 200 / np.pi) ) def calculate_meridian_term(calculation, h_b): """ Calculate meridian value from a measurement type using a Calculate object and H's baseline value. """ A1 = np.arcsin( calculation.residual / np.sqrt((calculation.ordinate.h + h_b) ** 2 + (calculation.ordinate.e) ** 2) ) A2 = np.arctan(calculation.ordinate.e / (calculation.ordinate.h + h_b)) A1 = (200 / np.pi) * (A1) A2 = (200 / np.pi) * (A2) meridian_term = calculation.angle + (calculation.pm * A1) - A2 return meridian_term def convert_to_geon(angle, include_seconds=True): """ Convert angles from measurements to geon """ degrees = int(angle) minutes = int((angle % 1) * 100) / 60 if include_seconds: seconds = ((angle * 100) % 1) / 36 else: seconds = 0 dms = (degrees + minutes + seconds) / 0.9 return dms