Exemple #1
0
def translate_input(dg, name):
    """Create SMT expressions for bounding the parameters of an input port
    to be within the constraints defined by the user

    :param name: Name of the port to be constrained
    :returns: None -- no issues with translating the port parameters to SMT
    """
    exprs = []
    if dg.size(name) <= 0:
        raise ValueError("Port %s must have 1 or more connections" % name)
    # Currently don't support this, and I don't think it would be the case
    # in real circuits, an input port is the beginning of the traversal
    if len(list(dg.predecessors(name))) != 0:
        raise ValueError("Cannot have channels into input port %s" % name)

    # If input is a type of node, call translate node
    [exprs.append(val) for val in translate_node(dg, name)]

    # Calculate flow rate for this port based on pressure and channels out
    # if not specified by user
    if not algorithms.retrieve(dg, name, 'min_flow_rate'):
        exprs.append(algorithms.calculate_port_flow_rate(dg, name))
    # TODO: Come up with a reasonable maximum pressure
    exprs.append((algorithms.retrieve(dg, name, 'flow_rate') < 100))

    # To recursively traverse, call on all successor channels
    #  for node_out in dg.succ[name]:
    #      [exprs.append(val) for val in translation_strats[
    #          algorithms.retrieve(dg, (name, node_out), 'kind')](dg, (name, node_out))]
    return exprs
def translate_chip(dg, name, dim):
    """Create SMT expressions for bounding the nodes to be within constraints
    of the overall chip such as its area provided

    :param name: Name of the node to be constrained
    :returns: None -- no issues with translating the chip constraints
    """
    exprs = []
    exprs.append(algorithms.retrieve(dg, name, 'x') >= dim[0])
    exprs.append(algorithms.retrieve(dg, name, 'y') >= dim[1])
    exprs.append(algorithms.retrieve(dg, name, 'x') <= dim[2])
    exprs.append(algorithms.retrieve(dg, name, 'y') <= dim[3])
    return exprs
def translate_output(dg, name):
    """Create SMT expressions for bounding the parameters of an output port
    to be within the constraints defined by the user

    :param str name: Name of the port to be constrained
    :returns: None -- no issues with translating the port parameters to SMT
    """
    exprs = []
    if dg.size(name) <= 0:
        raise ValueError("Port %s must have 1 or more connections" % name)
    # Currently don't support this, and I don't think it would be the case
    # in real circuits, an output port is considered the end of a branch
    if len(list(dg.succ[name])) != 0:
        raise ValueError("Cannot have channels out of output port %s" % name)

    # Since input is just a specialized node, call translate node
    [exprs.append(val) for val in translate_node(dg, name)]

    # Calculate flow rate for this port based on pressure and channels out
    # if not specified by user
    if not algorithms.retrieve(dg, name, 'min_flow_rate'):
        # The flow rate at this node is the sum of the flow rates of the
        # the channel coming in (I think, should be verified)
        total_flow_in = []
        for channel_in in dg.pred[name]:
            total_flow_in.append(dg.edges[(channel_in, name)]['flow_rate'])
        if len(total_flow_in) == 1:
            exprs.append(
                algorithms.retrieve(dg, name, 'flow_rate') == total_flow_in[0])
        else:
            total_flow_in_formulas = [
                a + b for a, b in zip(total_flow_in, total_flow_in[1:])
            ]
            exprs.append(
                algorithms.retrieve(dg, name, 'flow_rate') == logical_and(
                    *total_flow_in_formulas))
    return exprs
def translate_ep_cross(dg, name, fluid_name='default'):
    """Create SMT expressions for an electrophoretic cross
    :param str name: the name of the junction node in the electrophoretic cross
    :returns: None -- no issues with translating channel parameters to SMT
    :raises: ValueError if the analyte_properties are not defined properly
             TypeError if the analyte_properties are not floats or ints
    """

    # work in progress
    exprs = []

    # Validate input
    if dg.size(name) != 4:
        raise ValueError("Electrophoretic Cross %s must have 4 connections" %
                         name)

    # Electrophoretic Cross is a type of node, so call translate node
    [exprs.append(val) for val in translate_node(dg, name)]

    # Because it's done in translate_tjunc
    ep_cross_node_name = name

    # figure out which nodes are for sample injection and which are for separation channel
    # assume single input node, 3 output nodes, one junction node
    # assume separation and tail channels are specified by user
    phases = nx.get_edge_attributes(dg, 'phase')
    for edge, phase in phases.items():
        # assuming only one separation channel, and only 1 tail channel
        if phase == 'separation':
            separation_channel_name = edge
            anode_node_name = edge[1]
        elif phase == 'tail':
            tail_channel_name = edge
            cathode_node_name = edge[
                edge[0] ==
                ep_cross_node_name]  # returns whichever tuple element is NOT the ep_cross node

    # is there a better way to do this?
    node_kinds = nx.get_node_attributes(dg, 'kind')
    for node, kind in node_kinds.items():
        if node not in separation_channel_name and node not in tail_channel_name:
            if kind == 'input':
                injection_channel_name = (node, ep_cross_node_name)
                injection_node_name = node  # necessary?
            elif kind == 'output':
                waste_channel_name = (ep_cross_node_name, node)
                waste_node_name = node  # necessary?

    # assert dimensions:
    # assert width and height of tail channel to be equal to separation channel
    exprs.append(
        algorithms.retrieve(dg, tail_channel_name, 'width') ==
        algorithms.retrieve(dg, separation_channel_name, 'width'))
    exprs.append(
        algorithms.retrieve(dg, tail_channel_name, 'height') ==
        algorithms.retrieve(dg, separation_channel_name, 'height'))

    # assert width and height of injection channel to be equal to waste channel
    exprs.append(
        algorithms.retrieve(dg, injection_channel_name, 'width') ==
        algorithms.retrieve(dg, waste_channel_name, 'width'))
    exprs.append(
        algorithms.retrieve(dg, injection_channel_name, 'height') ==
        algorithms.retrieve(dg, waste_channel_name, 'height'))

    # assert height of separation channel and injection channel are same
    exprs.append(
        algorithms.retrieve(dg, injection_channel_name, 'height') ==
        algorithms.retrieve(dg, separation_channel_name, 'height'))

    # electric field
    E = Variable('E')
    exprs.append(E < 1000000)
    exprs.append(E > 0)
    exprs.append(E == algorithms.calculate_electric_field(
        dg, anode_node_name, cathode_node_name))
    # only works if cathode is an input?  only works for paths that are true in directed graph

    # assume that the analyte parameters were included in the injection port
    # need to validate that the data exists?

    D = algorithms.retrieve(dg, injection_node_name, 'analyte_diffusivities')
    C0 = algorithms.retrieve(dg, injection_node_name,
                             'analyte_initial_concentrations')
    q = algorithms.retrieve(dg, injection_node_name, 'analyte_charges')
    r = algorithms.retrieve(dg, injection_node_name, 'analyte_radii')

    analyte_properties = {
        'analyte_diffusivities': D,
        'analyte_initial_concentrations': C0,
        'analyte_charges': q,
        'analyte_radii': r
    }

    for property_name, values in analyte_properties.items():
        # check if something is defined, otherwise should be set to false
        if not values:
            raise ValueError(
                'No values defined for %s in electrophoretic cross node %s' %
                (property_name, ep_cross_node_name))

        # make sure all the values are either ints or floats
        if not all(isinstance(x, (int, float)) for x in values):
            raise TypeError(
                "%s values in electrophoretic cross node '%s' must be numbers"
                % (property_name, ep_cross_node_name))

    # n = number of analytes
    n = len(D)
    for property_name, values in analyte_properties.items():
        # check that they all have the same number of values
        n_to_check = len(values)

        if not (n_to_check == n):
            raise ValueError(
                "Expecting %s values, and found %s for %s in node: '%s'" %
                (n, n_to_check, property_name, ep_cross_node_name))

    delta = algorithms.retrieve(dg, separation_channel_name,
                                'min_sampling_rate')
    x_detector = algorithms.retrieve(dg, separation_channel_name, 'x_detector')

    # These are currently set as parameters to the node function
    # all are constants, numbers between 0 and 1
    # brief descriptions:
    # lower c, more discernable concentration peaks
    # higher p, any given conc. peak must be higher (closer to max conc.)
    # qf is arbitrary, rule of thumb qf = 0.9 (called q in Stephen Chou's paper)
    c = algorithms.retrieve(dg, ep_cross_node_name, 'c')
    p = algorithms.retrieve(dg, ep_cross_node_name, 'p')
    qf = algorithms.retrieve(dg, ep_cross_node_name, 'qf')

    mu = []
    v = []
    t_peak = []
    t_min = []
    W = algorithms.retrieve(dg, injection_channel_name, 'width')

    # for each analyte
    for i in range(0, n):
        # calculate mobility
        mu.append(Variable('mu_' + str(i)))
        exprs.append(mu[i] < 10000000000)
        exprs.append(mu[i] > 0)
        exprs.append(mu[i] == algorithms.calculate_mobility(
            dg, separation_channel_name, q[i], r[i]))

        # calculate velocity
        v.append(Variable('v_' + str(i)))
        #  exprs.append(v[i] < 1)
        exprs.append(v[i] > 0)
        exprs.append(v[i] == algorithms.calculate_charged_particle_velocity(
            dg, mu[i], E))

        # calculate t_peak, initialize variables for t_min
        t_peak.append(Variable('t_peak_' + str(i)))
        t_min.append(Variable('t_min_' + str(i)))
        exprs.append(t_peak[i] < 1000000)
        exprs.append(t_peak[i] > 0)
        exprs.append(t_min[i] < 1000000)
        exprs.append(t_min[i] > 0)
        exprs.append(t_peak[i] == x_detector / v[i])

    # detector position is somewhere along the separation channel
    # assume x_detector ranges from 0 to length of channel
    # to get absolute position of detector, add x_detector to ep_cross_node position
    exprs.append(x_detector <= algorithms.retrieve(dg, separation_channel_name,
                                                   'length'))

    # C_negligible is the minimum concentration level
    # i.e. smallest concentration peak should be > C_negligible
    C_negligible = Variable('C_negligible')
    C_floor = Variable('C_floor')
    sigma0 = Variable('sigma0')

    # TODO: This equation for sigma0 is for round, should add rectangular as well
    # definition of sigma0 for round channels (sigma0 ~ r_channel/2.355)
    exprs.append(sigma0 == W / (2 * 2.355))
    exprs.append(C_floor == (min(C0) /
                             (sigma0 +
                              (2 * max(D) * x_detector / v[n - 1])**0.5)))
    exprs.append(C_negligible == p * C_floor)

    diff = []
    for i in range(0, n - 1):

        # constrain that time difference between peaks is large enough to be detected
        exprs.append(t_peak[i] + delta < t_min[i])
        exprs.append(t_peak[i] + delta < t_min[i + 1])

        # constrain t_min to be where derivative of concentration is 0
        # if two adjacent peaks are close enough in height, then instead of using
        # the differential eqn, can approximate Fi(tmin) = Fi+1(tmin)
        # where i is the current analyte, and i+1 is the next analyte
        # and F = C(x_detector), C is concentration
        # quantify closeness of heights of peaks using the variable diff
        diff.append(Variable('diff_' + str(i)))
        exprs.append(diff[i] == C0[i] / C0[i + 1] * (D[i + 1] * mu[i] /
                                                     (D[i] * mu[i + 1]))**0.5)

        # if 0.1 < diff < 10, then use expression Fi(tmin) = Fi+1(tmin)
        # otherwise use expression dFi/dt (tmin) + dFi+1/dt (tmin) = 0
        t_min_constraint_expression = if_then_else(
            logical_and(0.1 < diff[i], diff[i] < 10),
            algorithms.calculate_concentration(dg, C0[i], D[i], W, v[i],
                                               x_detector, t_min[i]) -
            algorithms.calculate_concentration(dg, C0[i + 1], D[i + 1], W,
                                               v[i + 1], x_detector, t_min[i]),
            (algorithms.calculate_concentration(
                dg, C0[i + 1], D[i + 1], W, v[i + 1], x_detector,
                t_min[i])).Differentiate(t_min[i]) +
            (algorithms.calculate_concentration(
                dg, C0[i + 1], D[i + 1], W, v[i + 1], x_detector,
                t_min[i])).Differentiate(t_min[i]))

        exprs.append(t_min_constraint_expression == 0)

        # an alternate way to define C_negligible is:
        # C_negligible < p * min(Fi(t_peaki))
        # this requires computing the concentration again, which is inefficient
        # Wrote this expression just in case; this is the more exact expression
        #  for C_negligible, in case the simpler one does not work for square
        #  channels
        # I don't know how to use the min function in dreal, so I figured an
        #  equivalent but less efficient way to do it is just to ensure it is
        #  less than Fi(t_peaki), for every i
        #  exprs.append(C_negligible < p * algorithms.calculate_concentration(dg, C0[i], D[i], W, v[i], x_detector, t_peak[i]))

        # F(tmin, i)/(F(tmax, i)) <= c
        # F(tmin, i)/F(tpeak, j) ~ ( Fi(tmin,i) + Fi+1(tmin, i) + (n-2)(1-q)/(n-3) ) / Fj(tpeak,j)
        exprs.append(
            (algorithms.calculate_concentration(dg, C0[i], D[i], W, v[i],
                                                x_detector, t_min[i]) +
             algorithms.calculate_concentration(dg, C0[i + 1], D[i + 1], W, v[
                 i + 1], x_detector, t_min[i]) + (n - 2) * (1 - qf) /
             (n - 3) * C_negligible) / (algorithms.calculate_concentration(
                 dg, C0[i], D[i], W, v[i], x_detector, t_peak[i])) <= c)

        # F(tmin, i)/(F(tmax, i+1)) <= c
        exprs.append(
            (algorithms.calculate_concentration(dg, C0[i], D[i], W, v[i],
                                                x_detector, t_min[i]) +
             algorithms.calculate_concentration(dg, C0[i + 1], D[i + 1], W, v[
                 i + 1], x_detector, t_min[i]) + (n - 2) * (1 - qf) /
             (n - 3) * C_negligible) /
            (algorithms.calculate_concentration(dg, C0[i + 1], D[i + 1], W, v[
                i + 1], x_detector, t_peak[i + 1])) <= c)

    # Call translate on output - waste node
    [
        exprs.append(val) for val in translation_strats[algorithms.retrieve(
            dg, waste_node_name, 'kind')](dg, waste_node_name)
    ]
    # Call translate on output - anode
    [
        exprs.append(val) for val in translation_strats[algorithms.retrieve(
            dg, anode_node_name, 'kind')](dg, anode_node_name)
    ]

    return exprs
def translate_tjunc(dg, name, crit_crossing_angle=0.5):
    """Create SMT expressions for a t-junction node that generates droplets
    Must have 2 input channels (continuous and dispersed phases) and one
    output channel where the droplets leave the node. Continuous is usually
    oil and dispersed is usually water

    :param str name: The name of the channel to generate SMT equations for
    :param crit_crossing_angle: The angle of the dispersed channel to
        the continuous must be great than this to have droplet generation
    :returns: None -- no issues with translating channel parameters to SMT
    :raises: KeyError, if channel is not found in the list of defined edges
    """
    exprs = []
    # Validate input
    if dg.size(name) != 3:
        raise ValueError("T-junction %s must have 3 connections" % name)

    # Since T-junction is just a specialized node, call translate node
    [exprs.append(val) for val in translate_node(dg, name)]

    # Renaming for consistency with the other nodes
    junction_node_name = name
    # Since there should only be one output node, this can be found first
    # from the dict of successors
    try:
        output_node_name = list(dict(dg.succ[name]).keys())[0]
        output_channel_name = (junction_node_name, output_node_name)
    except KeyError as e:
        raise KeyError("T-junction must have only one output")
    # these will be found later from iterating through the dict of
    # predecessor nodes to the junction node
    continuous_node_name = ''
    continuous_channel_name = ''
    dispersed_node_name = ''
    dispersed_channel_name = ''

    # NetworkX allows for the creation of dicts that contain all of
    # the edges containing a certain attribute, in this case phase is
    # of interest
    phases = nx.get_edge_attributes(dg, 'phase')
    for pred_node, phase in phases.items():
        if phase == 'continuous':
            continuous_node_name = pred_node[0]
            continuous_channel_name = (continuous_node_name,
                                       junction_node_name)
            # assert width and height to be equal to output
            exprs.append(
                algorithms.retrieve(dg, continuous_channel_name, 'width') ==
                algorithms.retrieve(dg, output_channel_name, 'width'))
            exprs.append(
                algorithms.retrieve(dg, continuous_channel_name, 'height') ==
                algorithms.retrieve(dg, output_channel_name, 'height'))
        elif phase == 'dispersed':
            dispersed_node_name = pred_node[0]
            dispersed_channel_name = (dispersed_node_name, junction_node_name)
            # Assert that only the height of channel be equal
            exprs.append(
                algorithms.retrieve(dg, dispersed_channel_name, 'height') ==
                algorithms.retrieve(dg, output_channel_name, 'height'))
        elif phase == 'output':
            continue
        else:
            raise ValueError("Invalid phase for T-junction: %s" % name)

    # Epsilon, sharpness of T-junc, must be greater than 0
    # epsilon = 0.01*w for liquid droplets from Steijn et al.
    epsilon = Variable('epsilon')
    exprs.append(
        epsilon == algorithms.retrieve(dg, continuous_channel_name, 'width') *
        0.01)

    # TODO: Figure out why original had this cause it doesn't seem true
    #  # Pressure at each of the 4 nodes must be equal
    #  exprs.append(Equals(junction_node['pressure'],
    #                           continuous_node['pressure']
    #                           ))
    #  exprs.append(Equals(junction_node['pressure'],
    #                           dispersed_node['pressure']
    #                           ))
    #  exprs.append(Equals(junction_node['pressure'],
    #                           output_node['pressure']
    #                           ))

    # Viscosity in continous phase equals viscosity at output
    exprs.append(
        algorithms.retrieve(dg, continuous_node_name, 'viscosity') ==
        algorithms.retrieve(dg, output_node_name, 'viscosity'))

    # Flow rate into the t-junction equals the flow rate out
    exprs.append(
        algorithms.retrieve(dg, continuous_channel_name, 'flow_rate') +
        algorithms.retrieve(dg, dispersed_channel_name, 'flow_rate') ==
        algorithms.retrieve(dg, output_channel_name, 'flow_rate'))

    # Assert that continuous and output channels are in a straight line
    exprs.append(
        algorithms.channels_in_straight_line(dg, continuous_node_name,
                                             junction_node_name,
                                             output_node_name))

    # Droplet volume in channel equals calculated droplet volume
    # TODO: Manifold also has a table of constraints in the Schematic and
    # sets ChannelDropletVolume equal to dropletVolumeConstraint, however
    # the constraint is void (new instance of RealTypeValue) and I think
    # could conflict with calculated value, so ignoring it for now but
    # may be necessary to add at a later point if I'm misunderstand why
    # its needed
    exprs.append(
        algorithms.retrieve(dg, output_channel_name, 'droplet_volume') ==
        algorithms.calculate_droplet_volume(
            dg, algorithms.retrieve(dg, output_channel_name, 'height'),
            algorithms.retrieve(dg, output_channel_name, 'width'),
            algorithms.retrieve(dg, dispersed_channel_name, 'width'), epsilon,
            algorithms.retrieve(dg, dispersed_node_name, 'flow_rate'),
            algorithms.retrieve(dg, continuous_node_name, 'flow_rate')))

    # Assert critical angle is <= calculated angle
    cosine_squared_theta_crit = math.cos(math.radians(crit_crossing_angle))**2
    # Continuous to dispersed
    exprs.append(cosine_squared_theta_crit <= algorithms.cosine_law_crit_angle(
        dg, continuous_node_name, junction_node_name, dispersed_node_name))
    # Continuous to output
    exprs.append(cosine_squared_theta_crit <= algorithms.cosine_law_crit_angle(
        dg, continuous_node_name, junction_node_name, output_node_name))
    # Output to dispersed
    exprs.append(cosine_squared_theta_crit <= algorithms.cosine_law_crit_angle(
        dg, output_node_name, junction_node_name, dispersed_node_name))
    # Call translate on output
    [
        exprs.append(val) for val in translation_strats[algorithms.retrieve(
            dg, output_node_name, 'kind')](dg, output_node_name)
    ]
    return exprs
def translate_node(dg, name):
    """Create SMT expressions for bounding the parameters of an node
    to be within the constraints defined by the user

    :param name: Name of the node to be constrained
    :returns: None -- no issues with translating the port parameters to SMT
    """
    exprs = []
    # Pressure at a node is the sum of the pressures flowing into it
    output_pressures = []
    for node_name in dg.pred[name]:
        # This returns the nodes with channels that flowing into this node
        # pressure calculated based on P=QR
        # Could modify equation based on
        # https://www.dolomite-microfluidics.com/wp-content/uploads/
        # Droplet_Junction_Chip_characterisation_-_application_note.pdf
        output_pressures.append(
            algorithms.channel_output_pressure(dg, (node_name, name)))
    if len(dg.pred[name]) == 1:
        exprs.append(
            algorithms.retrieve(dg, name, 'pressure') == output_pressures[0])
    elif len(dg.pred[name]) > 1:
        output_pressure_formulas = [
            a + b for a, b in zip(output_pressures, output_pressures[1:])
        ]
        exprs.append(
            algorithms.retrieve(dg, name, 'pressure') == logical_and(
                *output_pressure_formulas))

    if algorithms.retrieve(dg, name, 'min_x'):
        exprs.append(
            algorithms.retrieve(dg, name, 'x') == algorithms.retrieve(
                dg, name, 'min_x'))
    else:
        exprs.append(algorithms.retrieve(dg, name, 'x') >= 0)
    if algorithms.retrieve(dg, name, 'min_y'):
        exprs.append(
            algorithms.retrieve(dg, name, 'y') == algorithms.retrieve(
                dg, name, 'min_y'))
    else:
        exprs.append(algorithms.retrieve(dg, name, 'y') >= 0)
    # If parameters are provided by the user, then set the
    # their Variable equal to that value, otherwise make it greater than 0
    if algorithms.retrieve(dg, name, 'min_pressure'):
        # If min_pressure has a value then a user defined value was provided
        # and this variable is set equal to this value, else simply set its
        # value to be >0, same for viscosity, pressure, flow_rate, X, Y and density
        exprs.append(
            algorithms.retrieve(dg, name, 'pressure') == algorithms.retrieve(
                dg, name, 'min_pressure'))
    else:
        exprs.append(algorithms.retrieve(dg, name, 'pressure') >
                     0.000001)  # Force pressure to be greater than 1uPa
        exprs.append(algorithms.retrieve(dg, name, 'pressure') <
                     1000000)  # Force pressure to be less than 1MPa
    if algorithms.retrieve(dg, name, 'min_flow_rate'):
        exprs.append(
            algorithms.retrieve(dg, name, 'flow_rate') == algorithms.retrieve(
                dg, name, 'min_flow_rate'))
    else:
        exprs.append(
            algorithms.retrieve(dg, name, 'flow_rate') >
            0.000000000001)  # Force flow rate to be greater than 1nL/s
        exprs.append(algorithms.retrieve(dg, name, 'flow_rate') <
                     0.001)  # Force flow rate to be less than 1L/s
    if algorithms.retrieve(dg, name, 'min_viscosity'):
        exprs.append(
            algorithms.retrieve(dg, name, 'viscosity') == algorithms.retrieve(
                dg, name, 'min_viscosity'))
    else:
        exprs.append(algorithms.retrieve(dg, name, 'viscosity') >
                     0.0001)  # Liquid helium is 0.000158
        exprs.append(algorithms.retrieve(dg, name, 'viscosity') <
                     100)  # Force viscosity to be less than 100Pa*s

    if algorithms.retrieve(dg, name, 'min_density'):
        exprs.append(
            algorithms.retrieve(dg, name, 'density') == algorithms.retrieve(
                dg, name, 'min_density'))
    else:
        exprs.append(algorithms.retrieve(dg, name, 'density') >
                     500)  # No liquid should be below this density
        exprs.append(algorithms.retrieve(dg, name, 'density') <
                     2000)  # Force density for be less than 2000kg/m^3

    densities = []
    for node_in in dg.pred[name]:
        densities.append(algorithms.retrieve(dg, node_in, 'density'))

    # If they are all equal, then set this node to be that density if there is a value
    # TODO: Create case for when different densities come in
    if densities and densities[1:] == densities[:-1]:
        exprs.append(
            algorithms.retrieve(dg, name, 'density') == algorithms.retrieve(
                dg,
                list(dg.pred[name].keys())[0], 'density'))
    # To recursively traverse, call on all successor channels
    for node_out in dg.succ[name]:
        [
            exprs.append(val)
            for val in translation_strats[algorithms.retrieve(
                dg, (name, node_out), 'kind')](dg, (name, node_out))
        ]
    return exprs
def translate_channel(dg, name):
    """Create SMT expressions for a given channel (edges in NetworkX naming)
    currently only works for channels with a rectangular shape, but should
    be expanded to include circular and parabolic

    :param str name: The name of the channel to generate SMT equations for
    :returns: None -- no issues with translating channel parameters to SMT
    :raises: KeyError, if channel is not found in the list of defined edges
    """
    exprs = []
    try:
        dg.edges[name]
    except KeyError:
        raise KeyError('Channel with ports %s was not defined' % name)

    # Create expression to force length to equal distance between end nodes
    exprs.append(algorithms.pythagorean_length(dg, name))

    # Set the length determined by pythagorean theorem equal to the user
    # provided number if provided, else assert that the length be greater
    # than 0, same for width and height
    if algorithms.retrieve(dg, name, 'min_length'):
        exprs.append(
            algorithms.retrieve(dg, name, 'length') == algorithms.retrieve(
                dg, name, 'min_length'))
    else:
        exprs.append(algorithms.retrieve(dg, name, 'length') >
                     0.000000001)  # Force to be greater than 1nm
        exprs.append(algorithms.retrieve(dg, name, 'length') <
                     1)  # Force to be less than 1m

    if algorithms.retrieve(dg, name, 'min_width'):
        exprs.append(
            algorithms.retrieve(dg, name, 'width') == algorithms.retrieve(
                dg, name, 'min_width'))
    else:
        exprs.append(algorithms.retrieve(dg, name, 'width') >
                     0.000000001)  # Force to be greater than 1nm
        exprs.append(algorithms.retrieve(dg, name, 'width') <
                     0.01)  # Force to be less than 1cm

    if algorithms.retrieve(dg, name, 'min_height'):
        exprs.append(
            algorithms.retrieve(dg, name, 'height') == algorithms.retrieve(
                dg, name, 'min_height'))
    else:
        exprs.append(algorithms.retrieve(dg, name, 'height') >
                     0.000000001)  # Force to be greater than 1nm
        exprs.append(algorithms.retrieve(dg, name, 'height') <
                     0.01)  # Force to be less than 1cm

    # Assert that viscosity in channel equals input node viscosity
    # Set output viscosity to equal input since this should be constant
    # This must be performed before calculating resistance
    exprs.append(
        algorithms.retrieve(dg, name, 'viscosity') == algorithms.retrieve(
            dg, algorithms.retrieve(dg, name, 'port_from'), 'viscosity'))
    #  exprs.append(algorithms.retrieve(dg, algorithms.retrieve(dg, name, 'port_to'), 'viscosity') ==
    #               algorithms.retrieve(dg, algorithms.retrieve(dg, name, 'port_from'), 'viscosity'))

    # Pressure at end of channel is lower based on the resistance of
    # the channel as calculated by calculate_channel_resistance and
    # pressure_out = pressure_in * (flow_rate * resistance)
    resistance_list = algorithms.calculate_channel_resistance(dg, name)

    # First term is assertion that each channel's height is less than width
    # which is needed to make resistance formula valid, second is the SMT
    # equation for the resistance, then assert resistance is >0
    exprs.append(resistance_list[0])
    resistance = resistance_list[1]
    #  exprs.append(algorithms.retrieve(dg, name, 'resistance') == resistance)
    exprs.append(algorithms.retrieve(dg, name, 'resistance') > 0)
    exprs.append(algorithms.retrieve(dg, name, 'resistance') < 1000000000
                 )  # Based on max pressure of 1MPa and flow rate of 0.001m^3/s

    # Assert flow rate equal to the flow rate coming in
    exprs.append(
        algorithms.retrieve(dg, name, 'flow_rate') == algorithms.retrieve(
            dg, algorithms.retrieve(dg, name, 'port_from'), 'flow_rate'))

    # Channels do not have pressure because it decreases across channel
    # Call translate on the output to continue traversing the channel
    [
        exprs.append(val) for val in translation_strats[algorithms.retrieve(
            dg, algorithms.retrieve(dg, name, 'port_to'), 'kind')](
                dg, algorithms.retrieve(dg, name, 'port_to'))
    ]
    return exprs