Esempio n. 1
0
 def process_input(attr_list):
     if attr_list[0][0] is None:
         raise ValueError("Propeller must have a name.")
     else:
         name = str(attr_list[0][0])
     if attr_list[1][0] is None:
         raise ValueError("Propeller must have a weight.")
     else:
         weight = {'value': convert_unit(float(attr_list[1][0]), str(attr_list[1][1]), 'std_metric'),
                   'unit': 'N'}
     if attr_list[2][0] is None:
         raise ValueError("Propeller must have a diameter.")
     else:
         diameter = {'value': convert_unit(float(attr_list[2][0]), str(attr_list[2][1]), 'std_metric'),
                     'unit': 'm'}
     if attr_list[3][0] is None:
         raise ValueError("Propeller must have a pitch.")
     else:
         pitch = {'value': convert_unit(float(attr_list[3][0]), str(attr_list[3][1]), 'std_metric'),
                  'unit': 'm'}
     if attr_list[4][0] is None:
         raise ValueError("Propeller must have a number of blades.")
     else:
         n_blades = int(attr_list[4][0])
     try:
         cost = float(attr_list[5][0])
     except TypeError:
         cost = None
     return [name, weight, diameter, pitch, n_blades, cost]
Esempio n. 2
0
 def new_unit_selection(event, obj_attr):
     """
     See displayable.Displayable.display_frame.new_unit_selection() for a description of this method.
     """
     this_cb = event.widget
     this_textvariable = current_obj_vars[obj_attr][1]
     new_unit = this_cb.get()
     this_attr_val = getattr(self, obj_attr)
     try:
         this_textvariable.set("%0.3f" % convert_unit(this_attr_val["value"], this_attr_val["unit"], new_unit))
     except CapacityConvError:
         voltage = self.voltage["value"]
         this_textvariable.set(
             "%0.3f" % convert_unit(this_attr_val["value"], this_attr_val["unit"], new_unit, voltage)
         )
Esempio n. 3
0
 def process_input(attr_list):
     if attr_list[0][0] is None:
         raise ValueError("Laser cutter must have a name.")
     else:
         name = str(attr_list[0][0])
     if attr_list[1][0] is None:
         raise ValueError("Laser cutter must have a length value.")
     else:
         length = {'value': convert_unit(float(attr_list[1][0]), str(attr_list[1][1]), 'std_metric'),
                   'unit': 'm'}
     if attr_list[2][0] is None:
         raise ValueError("Laser cutter must have a width value.")
     else:
         width = {'value': convert_unit(float(attr_list[2][0]), str(attr_list[2][1]), 'std_metric'),
                  'unit': 'm'}
     return [name, length, width]
 def process_input(attr_list):
     if attr_list[0][0] is None:
         raise ValueError("Printing material must have a name.")
     else:
         name = str(attr_list[0][0])
     if attr_list[1][0] is None:
         raise ValueError("Printing material must have a density.")
     else:
         density = {'value': convert_unit(float(attr_list[1][0]), str(attr_list[1][1]), 'std_metric'),
                    'unit': r'kg*m^-3'}
     if attr_list[2][0] is None:
         raise ValueError("Printing material must have a cross-sectional area.")
     else:
         cs_area = {'value': convert_unit(float(attr_list[2][0]), str(attr_list[2][1]), 'std_metric'),
                    'unit': r'm^2'}
     return [name, density, cs_area]
 def process_input(attr_list):
     if attr_list[0][0] is None:
         raise ValueError("Cutting material must have a name.")
     else:
         name = str(attr_list[0][0])
     if attr_list[1][0] is None:
         raise ValueError("Cutting material must have a density.")
     else:
         density = {'value': convert_unit(float(attr_list[1][0]), str(attr_list[1][1]), 'std_metric'),
                    'unit': r'kg*m^-3'}
     if attr_list[2][0] is None:
         raise ValueError("Cutting material must have thickness.")
     else:
         thickness = {'value': convert_unit(float(attr_list[2][0]), str(attr_list[2][1]),
                                            'std_metric'), 'unit': 'm'}
     return [name, density, thickness]
 def new_unit_selection(event, obj_attr):
     """
     This is an event handler for when a combobox value changes (i.e., the user wants to do a unit conversion).
     The function simply changes the value of the textvariable linked to the attribute entry according to the
     current value of the unit combobox.
     """
     this_cb = event.widget
     this_textvariable = current_obj_vars[obj_attr][1]
     new_unit = this_cb.get()
     old_unit = current_obj_vars[obj_attr][3]
     current_obj_vars[obj_attr][3] = new_unit
     this_attr_val = getattr(self, obj_attr)
     if master.master.mode == 'edit':
         this_textvariable.set("%0.3f" %
                               convert_unit(float(this_textvariable.get()), old_unit, new_unit))
     else:
         this_textvariable.set("%0.3f" % convert_unit(this_attr_val['value'], this_attr_val['unit'], new_unit))
Esempio n. 7
0
def hub_layout_simple(bat_dim, sensors):
    # Define battery and flight controller size. It has been determined that other electronic components such as the
    # GPS and receiver will not go on the hub.
    bat_size = [convert_unit(d, 'in', 'm') for d in bat_dim]
    fc_size = [convert_unit(d, 'in', 'm') for d in [2.77, 1.77, 0.53]]

    comp_sizes = [bat_size, fc_size]
    interior_sensor_zdims = []
    for sensor in sensors:
        sensor_size = [sensor.xdim['value'], sensor.ydim['value'], sensor.zdim['value']]
        comp_sizes.append(sensor_size)
        if sensor.req_layer is None:
            interior_sensor_zdims.append(sensor_size[2])
    comp_xy_sizes = [size[:2] for size in comp_sizes]

    # Determine the required size of the hub
    arm_dim = 1*0.0254  # Size of corner for arm connections in meters
    flat_comp_xy_sizes = [dim for size in comp_xy_sizes for dim in size]
    max_comp_dim = max(flat_comp_xy_sizes)
    # Start with a guess equal to the maximum component dimension plus a comfort factor. For most cases this will be the
    # battery max dimension and this guess will be the final hub size.
    hub_size = max_comp_dim * 1.05
    # However, it is concievable that a large, squareish component could still not fit in the hub after taking into
    # account the areas for the arm attachments, therefore we check the other components and resize if necessary.
    for size in comp_xy_sizes:
        if all((dim >= (hub_size-2*arm_dim) for dim in size)):
            hub_size = (min(size)+2*arm_dim)*1.05

    # Determine separation between hub layers. Since in the simplified hublayout model the battery will be placed on the
    # "ceiling" of the layer on which the flight controller is placed, the spacing needs to be at least equal to the
    # sum of their heights with some extra room built in.
    try:
        hub_separation = 1.5 * max((bat_size[2]+fc_size[2]), max(interior_sensor_zdims))
    except ValueError:
        hub_separation = 1.5 * (bat_size[2]+fc_size[2])

    # Since this is a simplifed version of the hublayout code, we will not try to create a grid showing the placement
    # of the components. In order to let the caller know how many layers there are (for use in figuring out the weight
    # of the aircraft) the "grid" is returned as a length 2 list. This is a workaround since the way the caller checks
    # how many layers there are is by checking the length of the grid list of lists returned from this function. This
    # will result in the code sizing the aircraft for a hub with two layers, which

    return convert_unit(hub_size, 'm', 'in'), convert_unit(hub_separation, 'm', 'in'), [None, None]
 def process_input(attr_list):
     motor = attr_list[0][0]
     prop = attr_list[1][0]
     test_bat_volt_rating = {'value': float(attr_list[2][0]), 'unit': 'V'}
     current_vec = {'value': [float(val) for val in attr_list[3]], 'unit': 'A'}
     voltage_vec = {'value': [float(val) for val in attr_list[4]], 'unit': 'V'}
     pwr_vec = {'value': [float(val) for val in attr_list[5]], 'unit': 'W'}
     rpm_vec = attr_list[6]
     throttle_vec = attr_list[7]
     thrust_val_list = [convert_unit(float(val), str(attr_list[8][1]), 'std_metric') for val in attr_list[8][0]]
     thrust_vec = {'value': thrust_val_list, 'unit': 'N'}
     return [motor, prop, test_bat_volt_rating, current_vec, voltage_vec, pwr_vec, rpm_vec, throttle_vec, thrust_vec]
Esempio n. 9
0
 def process_input(attr_list):
     if attr_list[0][0] is None:
         raise ValueError("Sensor must have a name.")
     else:
         name = attr_list[0][0]
     if attr_list[1][0] is None:
         raise ValueError("Sensor must have a weight.")
     else:
         weight = {'value': convert_unit(float(attr_list[1][0]), str(attr_list[1][1]), 'std_metric'), 'unit': 'N'}
     if attr_list[2][0] is None:
         raise ValueError("Sensor must have an X-dimension.")
     else:
         xdim = {'value': convert_unit(float(attr_list[2][0]), str(attr_list[2][1]), 'std_metric'), 'unit': 'm'}
     if attr_list[3][0] is None:
         raise ValueError("Sensor must have a Y-dimension.")
     else:
         ydim = {'value': convert_unit(float(attr_list[3][0]), str(attr_list[3][1]), 'std_metric'), 'unit': 'm'}
     if attr_list[4][0] is None:
         raise ValueError("Sensor must have a Z-dimension.")
     else:
         zdim = {'value': convert_unit(float(attr_list[4][0]), str(attr_list[4][1]), 'std_metric'), 'unit': 'm'}
     try:
         if attr_list[5][0] is None:
             req_layer = attr_list[5][0]
         elif attr_list[5][0].lower() in ['top', 'bottom']:
             req_layer = attr_list[5][0].lower()
         else:
             raise ValueError("Required layer must be 'top', 'bottom', or leave blank.")
     except AttributeError:
         raise ValueError("Required layer must be 'top', 'bottom', or leave blank.")
     try:
         if attr_list[6][0] is None:
             req_orient = attr_list[6][0]
         elif attr_list[6][0].lower() in ['forward', 'sideways']:
             req_orient = attr_list[6][0].lower()
         else:
             raise ValueError("Required orientation must be 'forward', 'sideways', or leave blank.")
     except AttributeError:
         raise ValueError("Required orientation must be 'forward', 'sideways', or leave blank.")
     return [name, weight, xdim, ydim, zdim, req_layer, req_orient]
Esempio n. 10
0
 def process_input(attr_list):
     if attr_list[0][0] is None:
         raise ValueError("Motor must have a name.")
     else:
         name = str(attr_list[0][0])
     if attr_list[1][0] is None:
         raise ValueError("Motor must have a weight.")
     else:
         weight = {'value': convert_unit(float(attr_list[1][0]), str(attr_list[1][1]), 'std_metric'),
                   'unit': 'N'}
     if attr_list[2][0] is None:
         raise ValueError("Motor must have a velocity constant.")
     else:
         Kv = {'value': float(attr_list[2][0]), 'unit': str(attr_list[2][1])}
     if attr_list[3][0] is None:
         raise ValueError("Motor must have a body diameter.")
     else:
         body_diameter = {'value': convert_unit(float(attr_list[3][0]), str(attr_list[3][1]),
                                                'std_metric'), 'unit': 'm'}
     try:
         cost = float(attr_list[4][0])
     except TypeError:
         cost = None
     return [name, weight, Kv, body_diameter, cost]
def is_feasible(quad_attrs, constraints):
    """
    Checks if a quadrotor alternative (described by its attributes passed in using the quad_attrs variable) is feasible
    considering the constraints given by the user by calculating some vehicle sizing and performance metrics.

    Returns ('true', vehicle_performance), where vehicle  performance is a list of metrics, if alternative is feasible.
    Returns (rejection reason, rejected value), where rejection reason is a string, if alternative is not
    feasible.
    """

    bat_xdim, bat_ydim, bat_zdim, prop_dia, motor_body_dia, cmat_density, cmat_thickness, \
        pmat_density, bat_weight, motor_weight, prop_weight, pmc_max_thrust, bat_voltage, bat_capacity, pmc_thrust_vec, \
        pmc_current_vec = quad_attrs

    endurance_req, payload_req, max_weight, max_size, maneuverability, \
        p_len, p_width, p_height, c_len, c_width, max_build_time, sensors = constraints

    # First estimate the vehicle size based on battery, prop, and motor. There are several constraints tested here.
    # 1) The maximum vehicle dimension must be less than the max_size constraint
    # 2) The dimension of the hub must be less than min(cutter_len, cutter_width)
    # 3) The length of the arm must be less than max(printer_len, printer_width). This assumes the other dimension of
    # the printer is sufficiently large, which is a fair assumption since the arm is long and narrow. This also assumes
    # the printer height is sufficient.

    # Call hublayout.hub_layout to size the hub for the chosen battery, sensors, and electronic component set (defined
    # within hublayout.hub_layout). See hublayout module for more information on the hub sizing function and what
    # hub_grid is. hub_layout returns the hub size in inches, the hub layer separation in inches, and the hub grid. Also
    # see hublayout.hublayout_simple
    try:
        hub_size, hub_separation, hub_grid = hublayout.hub_layout([bat_xdim, bat_ydim, bat_zdim], sensors)
    except hublayout.PlacementError as e:
        print str(e)
        return "Could not place sensors in/on hub.", 'N/A', None

    safe_factor = 1.15
    n_arms = 4
    prop_disc_separation_limited_len = safe_factor * (prop_dia/2/math.sin(math.pi/n_arms) + 0.75*motor_body_dia -
                                                      0.5*hub_size)
    prop_to_hub_limited_len = safe_factor * \
        (prop_dia/2 + 1.5*motor_body_dia/2)
    arm_len = max(prop_disc_separation_limited_len, prop_to_hub_limited_len)
    size = hub_size + 2*arm_len + prop_dia
    if size > max_size:
        return "Max dimension too large.", size, None
    if hub_size > min(c_len, c_width):
        return "Hub too large for cutter", hub_size, None
    if arm_len > max(p_len, p_width):
        return "Arms too long for printer.", arm_len, None

    n_layers = len(hub_grid)
    hub_area = hub_size**2 * n_layers
    hub_weight = cmat_density / 12**3 * cmat_thickness * hub_area
    hub_corner_len = hub_separation

    # I (Nate Beals) have no idea where this equation comes from. I pulled it straight from the VBA code.
    arm_vol_incube = (-0.59039*hub_separation**3) - (0.39684*arm_len*hub_corner_len**2) \
        + (0.10027*hub_corner_len*arm_len**2) + (2.35465*hub_separation**2) \
        + (2.06676*hub_corner_len**2) - (0.22142*arm_len*hub_corner_len) \
        - (9.04469e-2*arm_len**2) - (2.1687*hub_separation) - (0.9074*hub_corner_len) \
        + (0.92599*arm_len) - 0.99887
    arm_vol_ftcube = arm_vol_incube / (12**3)
    arm_weight = arm_vol_ftcube * pmat_density

    # The original authors give misc other weights for parts to go into the aggregate weight. Note: if the user wishes
    # to include sensors this weight should also be added.
    sensors_weight = convert_unit(sum(s.weight['value'] for s in sensors), 'N', 'lbf')
    wire_weight = 0.000612394 * arm_len * n_arms
    esc_weight = 0.2524
    apm_weight = 0.0705479
    compass_weight = 0.06062712
    receiver_weight = 0.033069
    propnut_weight = 0.0251327
    weights = [arm_weight*n_arms, compass_weight, receiver_weight, apm_weight, wire_weight, esc_weight, propnut_weight]
    weights += [hub_weight, bat_weight, motor_weight*n_arms, prop_weight*n_arms, sensors_weight]
    vehicle_weight = sum(weights)
    if vehicle_weight > max_weight:
        return "Too heavy.", vehicle_weight, None

    # Now estimate the thrust required for the vehicle based on the maneuverability requested by the user and the
    # weight that was just calculated. The original MASR Excel tool uses a table lookup to find the thrust margin
    # coefficient, but since there are only 3 options to choose from (Normal, High, Acrobatic), it makes more sense to
    # simply assign these three values without bothering looking them up in a table. If additional maneuverability
    # fidelity is desired it may be good to use the table lookup.
    thrust_margin_coef = [1.29, 1.66, 2.09][['Normal', 'High', 'Acrobatic'].index(maneuverability)]
    thrust_available = n_arms * pmc_max_thrust
    payload_capacity = (thrust_available / thrust_margin_coef) - vehicle_weight
    if payload_capacity < payload_req:
        return "Not enough payload capacity.", payload_capacity, None

    # Next, determine the estimated vehicle endurance and compare to the endurance required by the user. For this step
    # the average current draw needs to be interpolated from the propeller/motor combo current vs. thrust data using
    # the average thrust as the interpolation point of interest. The equation for average thrust given below assumes
    # that the mission consists only of hovering. This could be replaced with a real mission model result.
    avg_thrust = 1.125 * (vehicle_weight + payload_req) / n_arms
    try:
        avg_current = interp(pmc_thrust_vec, pmc_current_vec, avg_thrust)
    except ValueError as e:
        return str(e)
    vehicle_endurance = bat_capacity / (n_arms * avg_current * 1000) * 60
    if vehicle_endurance < endurance_req:
        return "Not enough endurance.", vehicle_endurance, None

    # Calculate estimated build time using equation from original MASR tool (point of origin unknown), and
    # compare to user requirement. Again, original equation in non-standard English units. Output of the build time
    # equation is hours.
    build_time = (-1.68036*arm_len*hub_separation**2 + 10.49405*hub_separation**2 +
                  4.85943*arm_len*hub_separation + 0.48171*arm_len*hub_corner_len -
                  29.25380*hub_separation - 2.59574*hub_corner_len - 3.27393*arm_len + 22.59885) * n_arms
    if build_time > max_build_time:
        return "Takes too long to build.", build_time, None

    # Since the alternative isn't infeasible by this point, it must be feasible. First convert some stuff back to metric
    size = convert_unit(size, 'in', 'm')
    vehicle_weight = convert_unit(vehicle_weight, 'lbf', 'N')
    payload_capacity = convert_unit(payload_capacity, 'lbf', 'N')
    hub_size = convert_unit(hub_size, 'in', 'm')
    hub_separation = convert_unit(hub_separation, 'in', 'm')
    arm_len = convert_unit(arm_len, 'in', 'm')
    vehicle_performance = [vehicle_weight, payload_capacity, vehicle_endurance, size, build_time]
    vehicle_geometry = [hub_size, hub_separation, hub_grid, arm_len]
    return 'true', vehicle_performance, vehicle_geometry
def unit_wrapper(quad):
    """
    This function takes a quadrotor object as input (from the generate_alternatives() function) and converts the
    attribute quantities that will be needed in the is_feasible() function to English units. This is done because the
    performance and sizing equations given in the original GT tool are in English units. Without re-deriving all of them
    without any documentation it is necessary to do the calculations in English units and convert the results back to
    metric. Not very elegant, but for now it will have to do.
    """
    this_quad = quad
    bat_xdim = convert_unit(this_quad.battery.xdim['value'], this_quad.battery.xdim['unit'], 'in')
    bat_ydim = convert_unit(this_quad.battery.ydim['value'], this_quad.battery.ydim['unit'], 'in')
    bat_zdim = convert_unit(this_quad.battery.zdim['value'], this_quad.battery.zdim['unit'], 'in')
    prop_dia = convert_unit(this_quad.prop.diameter['value'], this_quad.prop.diameter['unit'], 'in')
    motor_body_dia = convert_unit(this_quad.motor.body_diameter['value'], this_quad.motor.body_diameter['unit'], 'in')
    cmat_density = convert_unit(this_quad.cmaterial.density['value'], this_quad.cmaterial.density['unit'], 'lbf*ft^-3')
    cmat_thickness = convert_unit(this_quad.cmaterial.thickness['value'], this_quad.cmaterial.thickness['unit'], 'in')
    pmat_density = convert_unit(this_quad.pmaterial.density['value'], this_quad.pmaterial.density['unit'], 'lbf*ft^-3')
    bat_weight = convert_unit(this_quad.battery.weight['value'], this_quad.battery.weight['unit'], 'lbf')
    motor_weight = convert_unit(this_quad.motor.weight['value'], this_quad.motor.weight['unit'], 'lbf')
    prop_weight = convert_unit(this_quad.prop.weight['value'], this_quad.prop.weight['unit'], 'lbf')
    pmc_max_thrust = convert_unit(this_quad.pmcombo.max_thrust['value'], this_quad.pmcombo.max_thrust['unit'], 'lbf')
    bat_voltage = this_quad.battery.voltage['value']
    bat_capacity = convert_unit(this_quad.battery.capacity['value'], this_quad.battery.capacity['unit'], 'mAh',
                                this_quad.battery.voltage['value'])
    pmc_thrust_vec = [convert_unit(val, 'N', 'lbf') for val in this_quad.pmcombo.thrust_vec['value']]
    pmc_current_vec = this_quad.pmcombo.current_vec['value']

    quad_attrs = [bat_xdim, bat_ydim, bat_zdim, prop_dia, motor_body_dia, cmat_density, cmat_thickness, pmat_density,
                  bat_weight, motor_weight, prop_weight, pmc_max_thrust, bat_voltage, bat_capacity, pmc_thrust_vec,
                  pmc_current_vec]
    return quad_attrs
 def new_unit_selection(event, obj_attr):
     this_cb = event.widget
     this_textvariable = current_obj_vars[obj_attr][1]
     new_unit = this_cb.get()
     this_attr_val = getattr(self, obj_attr)
     this_textvariable.set("%0.3f" % convert_unit(this_attr_val['value'], this_attr_val['unit'], new_unit))
Esempio n. 14
0
    def process_input(attr_list):
        # Object must have a name
        if attr_list[0][0] is None:
            raise ValueError("Battery must have a name.")
        else:
            name = str(attr_list[0][0])
        battery_type = attr_list[1][0]
        try:
            cost = float(attr_list[6][0])
        except ValueError:
            cost = None
        except TypeError:
            cost = None
        if attr_list[7][0] is None:
            raise ValueError("Battery must have an X-dimension.")
        else:
            xdim = {"value": convert_unit(float(attr_list[7][0]), str(attr_list[7][1]), "std_metric"), "unit": "m"}
        if attr_list[8][0] is None:
            raise ValueError("Battery must have a Y-dimension.")
        else:
            ydim = {"value": convert_unit(float(attr_list[8][0]), str(attr_list[8][1]), "std_metric"), "unit": "m"}
        if attr_list[9][0] is None:
            raise ValueError("Battery must have a Z-dimension.")
        else:
            zdim = {"value": convert_unit(float(attr_list[9][0]), str(attr_list[9][1]), "std_metric"), "unit": "m"}

        # Apply known relationships for LiPo batteries
        if battery_type is None or battery_type.lower() == "lipo":
            # Take care of voltage and cell inputs
            if attr_list[4][0] is None and attr_list[5][0] is None:
                raise ValueError("Battery must have a rated voltage or number of cells.")
            elif attr_list[4][0] is None and attr_list[5][0] is not None:
                cells = int(attr_list[5][0])
                voltage = {"value": cells * 3.7, "unit": str(attr_list[4][1])}
            elif attr_list[4][0] is not None and attr_list[5][0] is None:
                voltage = {"value": float(attr_list[4][0]), "unit": str(attr_list[4][1])}
                cells = int(round(voltage["value"] / 3.7))
            else:
                voltage = {"value": float(attr_list[4][0]), "unit": str(attr_list[4][1])}
                cells = int(attr_list[5][0])

            a = 4.04
            b = 139
            c = 0.0155
            # Object must be initialized with a capacity and/or a weight
            if attr_list[2][0] is None and attr_list[3][0] is None:
                raise ValueError("Battery must have a capacity and/or weight.")
            # If weight is not given but capacity is, solve Gur and Rosen quadratic for mass and convert to weight
            elif attr_list[2][0] is None:
                capacity = {
                    "value": convert_unit(float(attr_list[3][0]), str(attr_list[3][1]), "std_metric", voltage["value"]),
                    "unit": "Wh",
                }
                quad_sqrt = (b ** 2 - 4 * a * (c - capacity["value"])) ** 0.5
                if quad_sqrt > abs(b):
                    mass = {"value": (-b + quad_sqrt) / (2 * a), "unit": "kg"}
                    weight = {"value": mass["value"] * 9.81, "unit": "N"}
                else:
                    raise ValueError("Battery capacity out of range")
            # If weight is given but capacity is not, solve Gur and Rosen quadratic for capacity
            elif attr_list[3][0] is None:
                weight = {
                    "value": convert_unit(float(attr_list[2][0]), str(attr_list[2][1]), "std_metric"),
                    "unit": "N",
                }
                mass = {"value": weight["value"] / 9.81, "unit": "kg"}
                capacity = {"value": a * mass["value"] ** 2 + b * mass["value"] + c, "unit": "Wh"}
            else:
                weight = {
                    "value": convert_unit(float(attr_list[2][0]), str(attr_list[2][1]), "std_metric"),
                    "unit": "N",
                }
                mass = {"value": weight["value"] / 9.81, "unit": "kg"}
                capacity = {
                    "value": convert_unit(float(attr_list[3][0]), str(attr_list[3][1]), "std_metric", voltage["value"]),
                    "unit": "Wh",
                }
        else:  # For other battery types assume all information is given
            try:
                weight = {"value": convert_unit(float(attr_list[2][0]), attr_list[2][1], "std_metric"), "unit": "N"}
                mass = {"value": weight["value"] / 9.81, "unit": "kg"}
                voltage = {"value": float(attr_list[4][0]), "unit": "V"}
                capacity = {
                    "value": convert_unit(float(attr_list[3][0]), attr_list[3][1], "std_metric", voltage["value"]),
                    "unit": "Wh",
                }
                cells = int(attr_list[5][0])
            except TypeError:
                raise ValueError("If entering non-LiPo battery, must give all info.")
        return [name, battery_type, weight, mass, capacity, voltage, cells, cost, xdim, ydim, zdim]
Esempio n. 15
0
def hub_layout(bat_dim, sensors):
    """
    The goal here is to place all of the required components within a hub of the footprint given by hub_size X hub_size.
    Hub size is determined using the sizes of the components required to go on/in the hub.

    In addition, right now I am hard-coding the sizes of the flight controller, receiver, gps, and ESC into this
    function. In the future these sizes can be easily passed in on a call-by-call basis if for instance the user has a
    choice of various ESCs, receivers, etc.

    The input bat_dim is a list defining the size of the battery of the form [xdim, ydim, zdim] in inches.

    The input sensors is a list of sensor objects the user requires to be on the vehicle.
    """

    def is_occupied(this_place, this_size, mode='check'):
        """
        This function takes a starting point on the grid (representing the center of the rectangle placement) and checks
        whether the space that would be occupied by the rectangle centered on that spot is available for occupation.
        If the rectangle would overlap any occupied grid points, or if it would partially lay outside of the grid, the
        function returns True (i.e., the space is occupied). If all points that the rectangle would occupy are within
        the grid and unoccupied, the function returns False.

        The mode input can either be 'check' (default) or 'mark'. If mode is set to 'check', the function will simply
        check whether or not the rectangle defined by 'this_size' can be placed at location 'this_place', returning True
        or False appropriately. If the mode is set to 'mark', the function will mark the grid area defined by a
        rectangle defined by 'this_size' centered at 'this_place' as occupied (i.e., changes all grid point values to
        1). The 'mark' mode should only be used if it has already been established the grid area is clear, because it
        changes the value of the grid points before checking if the entire area is unoccupied.
        """
        l = this_place[2]
        if this_place[3] == 'forward':
            top_row = this_place[0] - int(math.ceil(this_size[0]/2/delta_p))
            bot_row = this_place[0] + int(math.ceil(this_size[0]/2/delta_p))
            left_col = this_place[1] - int(math.ceil(this_size[1]/2/delta_p))
            rght_col = this_place[1] + int(math.ceil(this_size[1]/2/delta_p))
        else:
            top_row = this_place[0] - int(math.ceil(this_size[1]/2/delta_p))
            bot_row = this_place[0] + int(math.ceil(this_size[1]/2/delta_p))
            left_col = this_place[1] - int(math.ceil(this_size[0]/2/delta_p))
            rght_col = this_place[1] + int(math.ceil(this_size[0]/2/delta_p))

        for r in xrange(top_row, bot_row+1):
            for c in xrange(left_col, rght_col+1):
                try:
                    # If grid point is already occupied, return True
                    if grid[l][r][c]:
                        return True
                    elif mode == 'mark':
                        grid[l][r][c] = 1
                # If index is out of the grid, return True.
                except IndexError:
                    return True
        # If no points were occupied or out of the grid, return False
        return False

    def find_place(this_size, pref_layer, layer_req=False, req_orient=None):
        """
        This function finds the place where the rectangle defined by 'this_size' is closest to the center of a
        layer, starting with the preferred layer given by 'pref_layer'. The layers then progress like pref_layer-1 -->
        pref_layer+1 --> pref_layer-2 --> etc. If layer preference changes then this order can be changed. Optimization
        of the layout gets more complicated if one does not place the battery and flight controller in the center of
        layers 1 and 2 (indices 0 and 1) respectively. Weight distributions may also become a consideration if the
        components have significantly different weight/area on the grid.

        The inputs 'in_layer_list' and 'req_orient' are for use when placing sensors which have specific requirements
        for where they are located on/in the hub.
        """
        if pref_layer is None:
            pref_layer = 1
        # First create list of layers to try in order of preference
        layer_list = []
        for l in xrange(n_layers):
            layer_list.append(pref_layer-l)
            if l != 0:
                layer_list.append(pref_layer+l)
        layer_list = [l for l in layer_list if 0 <= l < n_layers]

        if layer_req:
            layer_list = [pref_layer]

        if req_orient is not None:
            orient_list = [req_orient]
        else:
            orient_list = ['forward', 'sideways']

        for current_layer in layer_list:
            for orient in orient_list:
                # We can eliminate many grid points before brute forcing because we know the rectangle cannot lay
                # outside the grid.
                if orient == 'forward':
                    start_row = int(this_size[0]/2/delta_p)
                    end_row = n_points - int(this_size[0]/2/delta_p)
                    start_col = int(this_size[1]/2/delta_p)
                    end_col = n_points - int(this_size[1]/2/delta_p)
                else:
                    start_row = int(this_size[1]/2/delta_p)
                    end_row = n_points - int(this_size[1]/2/delta_p)
                    start_col = int(this_size[0]/2/delta_p)
                    end_col = n_points - int(this_size[0]/2/delta_p)
                all_places = [(r, c, current_layer, orient)
                              for r in xrange(start_row, end_row+1) for c in xrange(start_col, end_col+1)]
                # Rank list based on distance from center
                all_places_ranked = rank_places(all_places)
                # Starting with place closest to center, check is_occupied
                for current_place in all_places_ranked:
                    if not is_occupied(current_place, this_size):
                        return current_place
        return False

    def rank_places(places):
        """
        Rank the available places for the rectangle according to their distance from the center (closeness to the center
        is preferred). I thought this was a simple and good way to achieve the reasonable configuration, without taking
        into account the materials the rectangles are made out of (i.e., the weight distribution resulting from the
        arrangement).
        """
        def dist2center(place):
            return ((place[0]-center)**2 + (place[1]-center)**2)**0.5
        return sorted(places, key=dist2center)

    def find_top_layer(g):
        """
        This function takes in a grid (as defined above) and returns the uppermost layer index containing a
        component(s). It determines if a layer is occupied by computing the number of occupied points minus the number
        of points occupied by arm attachments (an arm_dim X arm_dim square in each layer corner).
        """
        i = len(g) - 1
        for l in reversed(g):
            flat_l = [c for r in l for c in r]
            n_occupied_pts = flat_l.count(1)
            n_corner_pts = 4*(math.ceil(arm_dim/delta_p) + 1)**2
            if n_occupied_pts > n_corner_pts:
                return i
            i -= 1

    #################################################################################################################

    # First define the size of the required electronic components that will go on the hub. Convert to meters.
    bat_size = [convert_unit(d, 'in', 'm') for d in bat_dim]
    fc_size = [convert_unit(d, 'in', 'm') for d in [2.77, 1.77, 0.53]]
    # receiver_size = [convert_unit(d, 'in', 'm') for d in [1.57, 1.06, 0.35]]
    # gps_size = [convert_unit(d, 'in', 'm') for d in [1.49, 1.49, 0.33]]
    # esc_size = [convert_unit(d, 'in', 'm') for d in [2.5, 2.25, 0.325]]

    non_sensor_sizes = [bat_size, fc_size]
    comp_sizes = [bat_size, fc_size]
    interior_sensor_zdims = []
    for sensor in sensors:
        sensor_size = [sensor.xdim['value'], sensor.ydim['value'], sensor.zdim['value']]
        comp_sizes.append(sensor_size)
        if sensor.req_layer is None:
            interior_sensor_zdims.append(sensor_size[2])
    comp_xy_sizes = [size[:2] for size in comp_sizes]

    # Determine the required size of the hub
    arm_dim = 1*0.0254  # Size of corner for arm connections in meters
    flat_comp_xy_sizes = [dim for size in comp_xy_sizes for dim in size]
    max_comp_dim = max(flat_comp_xy_sizes)
    # Start with a guess equal to the maximum component dimension plus a comfort factor. For most cases this will be the
    # battery max dimension and this guess will be the final hub size.
    hub_size = max_comp_dim * 1.05
    # However, it is concievable that a large, squareish component could still not fit in the hub after taking into
    # account the areas for the arm attachments, therefore we check the other components and resize if necessary.
    for size in comp_xy_sizes:
        if all((dim >= (hub_size-2*arm_dim) for dim in size)):
            hub_size = (min(size)+2*arm_dim)*1.05

    # Determine separation between hub layers using maximum z dimension of components, other than sensors that are
    # required to be mounted to the top or bottom of the hub.
    hub_separation = 1.5 * max([size[2] for size in non_sensor_sizes] + interior_sensor_zdims)

    # Create hub grid 101 x 101 x 4. Initialized to zero (all space is unoccupied)
    n_points = 101  # Odd number chosen so there is a center grid point located at (50, 50) in 2D
    n_layers = 4
    grid = [[[0 for i in xrange(n_points)] for j in xrange(n_points)] for k in xrange(n_layers)]
    center = int((n_points-1)/2)    # Or just n_points/2 ehhh..
    delta_p = hub_size/(n_points-1)

    # Mark off space for arms to be connected to the hub. For now these will be 1 inch in each corner per the GT tool.
    for layer in xrange(n_layers):
        for row in xrange(n_points):
            if (hub_size/n_points*row <= arm_dim) or (hub_size/n_points*row > hub_size-arm_dim-delta_p):
                for col in xrange(n_points):
                    if (hub_size/n_points*col <= arm_dim) or (hub_size/n_points*col > hub_size-arm_dim-delta_p):
                        grid[layer][row][col] = 1

    # Place flight controller in the center of layer 1.
    fc_place = [center, center, 0, 'forward']
    is_occupied(fc_place, fc_size, mode='mark')

    # Place battery in the second layer of the grid. In reality this is actually the top of the first layer
    # (or equivalently the bottom of the second layer). In the base configuration the flight controller will be mounted
    # to the floor of the first layer, and the battery will be mounted to the roof of the layer above. The format for
    # the place lists are as follows:
    # bat_place = [row_index, column_index, layer_index, orientation] location of center of component
    # Orientation refers to the direction of the longest dimension.
    bat_place = [center, center, 1, 'forward']
    is_occupied(bat_place, bat_size, mode='mark')

    # Now place all of the sensors the user has selected. If the sensor has a required layer the logic checks to see if
    # that layer is available. If the layer is not available, the alternative will fail. Currently only one sensor can
    # be on the top and bottom layers. This is because I am assuming that the sensor will need a full 360 deg view which
    # could be impeded by allowing more than one sensor on the top or bottom layer.
    for sensor in sensors:
        sensor_size = [sensor.xdim['value'], sensor.ydim['value'], sensor.zdim['value']]
        top_layer = find_top_layer(grid)
        if sensor.req_layer == 'top':
            if top_layer < n_layers-1:
                sensor_req_layer = top_layer + 1
                layer_required = True
            else:
                raise PlacementError("Could not place %s on top layer of hub. Top layer filled." % sensor.name)
        elif sensor.req_layer == 'bottom':
            if top_layer < n_layers-1:
                grid.insert(0, grid.pop())
                sensor_req_layer = 0
                layer_required = True
            else:
                raise PlacementError("Could not place %s on bottom layer of hub. All layers filled" % sensor.name)
        else:
            sensor_req_layer = None
            layer_required = False
        sensor_req_orient = sensor.req_orient
        sensor_place = find_place(sensor_size, sensor_req_layer, layer_required, sensor_req_orient)
        if not sensor_place:
            raise PlacementError("Could not place %s on hub." % sensor.name)
        is_occupied(sensor_place, sensor_size, mode='mark')

    # Return the hub size (in inches), the hub separation (in inches) and the grid, trimming unoccupied layers.
    current_top_layer = find_top_layer(grid)
    return convert_unit(hub_size, 'm', 'in'), convert_unit(hub_separation, 'm', 'in'), grid[:current_top_layer+1]