AJMR-Python-Baird/Mustique/adcptool/adcpprocess.py

344 lines
12 KiB
Python

import sys
sys.dont_write_bytecode = True
import math
from adcptool.vector import Vector
#
# handy functions
#
def compute_direction_angle(vec):
''' compute the direction angle based on direction vector vector
'''
# two points --> direction angle
dx = vec.x
dy = vec.y
# handling the quadrants
if dy == 0 and dx == 0:
# raise NotImplementedError('zero length vectors dont have any direction')
return 0
if dy == 0 and dx > 0:
# "0"
return 0.5 * math.pi
elif dy == 0 and dx < 0:
# "0"
return 1.5 * math.pi
elif dy < 0 and dx < 0:
# II + III
return math.atan(dx/dy) + math.pi
elif dy < 0 and dx > 0:
# IV
return math.atan(dx/dy) + math.pi
else:
# I
return math.atan(dx/dy)
def unitconversion(unit,p):
''' return the conversion factor from [unit]^p to [meters]^p
'''
# factors from X to meter
d = dict(cm=0.01, ft=0.3048)
return math.pow(d[unit],p)
def print_type(o):
''' print out the type of the given thing. useful for debugging.
'''
print(type(o).__name__)
#
# the actual objects
#
class ProcessedProfileObj():
''' holds a stack of finished (processed) ensembles
(and some extra data)
'''
#
# methods needed for defining the profile itself
def compute_direction_vector(self, a, b=False):
if b != False:
# two vectors --> direction vector
return (b - a).norm()
else:
# angle --> direction vector
return Vector(math.cos(a), math.sin(a))
def compute_direction_angle(self, vec):
# two points --> direction angle
dx = vec.x
dy = vec.y
# handling the quadrants
if dy == 0 and dx > 0:
# "0"
return 0.5 * math.pi
elif dy == 0 and dx < 0:
# "0"
return 1.5 * math.pi
elif dy < 0 and dx < 0:
# II + III
return math.atan(dx/dy) + math.pi
elif dy < 0 and dx > 0:
# IV
return math.atan(dx/dy) + math.pi
else:
# I
return math.atan(dx/dy)
def update_velocities(self, vm=None, vgm=None):
''' write back modified velocities into this object.
arguments:
vm ... velocity matrix. a 3d numpy array
vgm ... good value matrix belonging to vm. boolean numpy array, True if value is good, bad otherwise.
should match the masking in this object.
'''
# if (vm.all() == None) or (vgm.all() == None):
if (vm.all() == None) | (vgm.all() == None):
# only update vepth averaged velocities.
for i in range(len(self.ensembles)):
if not self.ensembles[i].void:
self.ensembles[i].velocity = self.ensembles[i].comp_avg_velocity()
else:
(rows, columns, dimension) = vm.shape
# velocities in cells
for i in range(columns):
if not self.ensembles[i].void: # for each ensemble
for r in range(len(self.ensembles[i].cells)):
self.ensembles[i].cells[r].velocity = Vector(vm[r,i].tolist())
# depth averaged velocities in ensembles
self.ensembles[i].velocity = self.ensembles[i].comp_avg_velocity()
def __init__(self, rawprofile, processing_settings, global_profile_position = False):
''' object to store the processed ADCP data.
attributes for this object:
self.start_dir direction of the profile, given in [rad], starting from north, clockwise
self.start_dir_vec unit vector version of the above, in X/Y coordinates
self.start_pos Vector() in X/Y/Z where the profile starts
self.start_offset distance from the first ensemble to the start_pos
(its a Vector() too; useful if there is no start_dir given and when computing absolute coordinates)
self.ensembles list where the ProcessedEnsembleObj()'s will be stored
'''
self._cfg = processing_settings # saved for later usage
#
# define the profile which will eventually be used
if global_profile_position != False:
self.start_pos = global_profile_position['start']
if 'offset' in global_profile_position:
self.start_offset = global_profile_position['offset']
else:
self.start_offset = Vector(0,0)
if 'dir' in global_profile_position:
# angle is defined, compute direction vector
self.start_dir = global_profile_position['dir']
self.start_dir_vec = self.compute_direction_vector(self.start_dir)
elif 'end' in global_profile_position:
# 2nd point was given, compute angle and direction vector
self.start_dir_vec = self.compute_direction_vector(global_profile_position['start'], global_profile_position['end'])
self.start_dir = self.compute_direction_angle(self.start_dir_vec)
else:
# no hint was given, use last ensemble as 2nd point and then do the same as above
endpos_n = rawprofile.ensembles[len(rawprofile.ensembles)-1].total_distance_traveled_north
endpos_e = rawprofile.ensembles[len(rawprofile.ensembles)-1].total_distance_traveled_east
self.start_dir_vec = (Vector(endpos_e, endpos_n) - self.start_offset).norm()
self.start_dir = self.compute_direction_angle(self.start_dir_vec)
#
# process ensembles
self.ensembles = []
for ensemble in rawprofile.ensembles:
self.ensembles.append(ProcessedEnsembleObj(ensemble, self.start_pos, self.start_dir, self.start_dir_vec, self.start_offset, processing_settings))
self.cell_height = rawprofile.depth_cell_length * 0.01 # in the ascii file its always in cm, here its always in m, so this can be hard coded.
class ProcessedEnsembleObj():
'''
object to hold the finished (processed) ensemble, ready for the output
attributes:
self.velocity a Vector() that holds the velocity in U/V coordinates
sel.position a Vector() that holds the position of the ensemble in X/Y/Z
(currently we have self.position_{a,b,c} for different methods to store the things)
'''
def comp_avg_velocity(self, method=0):
''' compute and return depth averaged velocity
kwargs:
method int define method for averaging. 0 is the only supported one
'''
if method == 0:
sum_v_comp = Vector(0,0,0)
for i in range(len(self.cells)):
sum_v_comp += self.cells[i].velocity
return sum_v_comp / len(self.cells)
else:
raise Exception('method no {} not implemented'.format(method))
def __init__(self, ensemble, profile_start, profile_dir, profile_dir_vec, profile_offset, settings):
#
# velocity processing
# its the conversion factor from the unit defined in the raw data to meters.
conversionfactor = unitconversion(ensemble.measurement_units, 1)
if ensemble.number_of_good_cells != 0:
# sometimes there is no good cell and hence no velocity,
# (needed if someone wants to skip these)
self.void = False
# process the velocity in each cell
if 'dimensions' not in settings or settings['dimensions'] == 3:
self.cells = []
for c in ensemble.cells:
self.cells.append(ProcessedCellObj(
c, profile_start, profile_dir, profile_dir_vec,
profile_offset, settings, conversionfactor))
# compute the averaged velocity
if 'dimensions' not in settings or settings['dimensions'] == 2:
self.velocity = Vector()
# get averaged velocity (should be a Vector object)
avg_method = settings['avg_method'] if 'avg_method' in settings else 0
self.velocity = self.comp_avg_velocity(avg_method)
# rotation
if 'uv_rot' in settings and settings['uv_rot']:
#trans_uvrot(profile_dir)
base_rot = math.pi / 2
self.velocity = self.velocity.rot(base_rot + profile_dir)
else:
self.void = True
#
# position processing
# processing the 4 depths and averaging
self.four_depths = []
self.four_depths += ensemble.depth_reading
self.depth = sum(ensemble.depth_reading) / 4
# z-value of the river ground for this ensemble
#height = Vector(0,0, profile_start.z - self.depth)
height = Vector(0,0, profile_start.z)
dmg = ensemble.total_distance_made_good
self.dmg = dmg
if settings['proj_method'] == 1:
# position on the profile (based on total distance made good)
self.position = profile_dir_vec * dmg + profile_start + height
elif settings['proj_method'] == 2:
# position according to movement data
self.position = Vector(
ensemble.total_distance_traveled_east,
ensemble.total_distance_traveled_north) \
- profile_offset + profile_start + height
elif settings['proj_method'] == 3:
# position from movement data, projected on the profile
self.position = Vector(
ensemble.total_distance_traveled_east,
ensemble.total_distance_traveled_north).proj(profile_dir_vec) \
- profile_offset + profile_start + height
#p3 = (p2-profile_start).proj(profile_dir_vec) + profile_start
else:
raise NotImplementedError('you specified an invalid (or no) proj_method!')
class ProcessedCellObj():
def __init__(self, cell, profile_start, profile_dir, profile_dir_vec, profile_offset, settings, conversionfactor):
'''
object to hold a finished (processed) cell , ready for the output
attributes:
self.velocity a Vector() that holds the velocity in u/v/w coordinates
self.z_position a Vector() that holds the position of the cell in x/y/z coords
'''
# it's a measured cell
self.artificial = False
#
# velocity processing again
self.velocity = Vector()
# switch coordinates
# self.velocity.x = cell.velocity_comp.y # east value
# self.velocity.y = cell.velocity_comp.x # north value
# self.velocity.z = cell.velocity_comp.z # up value
self.velocity = Vector(cell.velocity_comp.x, cell.velocity_comp.y, cell.velocity_comp.z)
# rotation
if 'uv_rot' in settings and settings['uv_rot']:
# base_rot = math.pi / 2
self.velocity = self.velocity.rot(profile_dir)
# unit conversion
self.velocity *= conversionfactor
#
# position processing
self.z_position = profile_start[2] - (cell.depth)
if 'flip_z' in settings and settings['flip_z']:
self.z_position *= -1
class ArtificialCellObj():
def __init__(self, position, velocitiy):
'''
like ProcessedCellObj but created from extrapolated data.
'''
self.artificial = True
self.z_position = position
self.velocity = velocitiy