Studio 7 โ€” Functions: Decomposition & Abstraction

ENGR 103 โ€” Week 7

๐Ÿ“‹ Contents

Studio Overview0:00โ€“0:10 โ€” Warm-Up0:10โ€“0:35 โ€” Part 1 โ€” Codedex Warmup (Guided)0:35โ€“1:10 โ€” Part 2 โ€” Practical Lab: Polynomial Function Library1:10โ€“1:25 โ€” Part 3 โ€” Extension: Bisection Inside a Function1:25โ€“1:30 โ€” Wrap-Up & TA Check-Off

Student Instructions | Week 7 | Topics: def, return, docstrings, default args, assertions

Oregon State University | ENGR 103 | Dr. Cheng Li

Studio Overview

0:00โ€“0:10 โ€” Warm-Up

What is wrong with this code? Fix it.

Debug

Type and run this code:

def calculate_power(V, I)
    power = V * I * 1000
    Print(power)

calculate_power(0.45 0.020)
โœ๏ธ Your Task:

Find and fix all errors (there are 4). List each error and your fix on the lines below.

0:10โ€“0:35 โ€” Part 1 โ€” Codedex Warmup (Guided)

Type and run each snippet.

Exercise 7A โ€” Function with Docstring and Return

This function adds up the powers of a number: 1 + base + baseยฒ + โ€ฆ + base^n. It uses a for loop inside a def. Type and run this code:

def power_sum(base, n):
    """
    Compute 1 + base + base^2 + ... + base^n.

    Args:
        base : The base number
        n    : Highest exponent (must be >= 0)
    Returns:
        float: Sum of the (n+1) terms
    """
    assert n >= 0, f'n must be >= 0, got {n}'
    total = 0.0
    for i in range(n + 1):
        total += base ** i
    return total

# Test: base=2 gives 1 + 2 + 4 + 8 + ...
for n in range(0, 6):
    s = power_sum(2, n)
    print(f'n={n}: power_sum(2, {n}) = {s:.0f}')

# Read the docstring
help(power_sum)
โœ๏ธ Your Task:

What happens when you call power_sum(2, -1)? What does the AssertionError message say? Why is that check useful?

Exercise 7B โ€” Default Arguments & Multiple Returns

The vertex of a parabola y = axยฒ + bx + c is at x = โˆ’b/(2a). The decimals parameter shows how to give an argument a default value. Type and run this code:

def parabola_vertex(a, b, c, decimals=4):
    """
    Compute the vertex of y = ax^2 + bx + c.

    Args:
        a        : Coefficient of x^2 (must be non-zero)
        b        : Coefficient of x
        c        : Constant term
        decimals : Decimal places for rounding (default 4)
    Returns:
        tuple: (x_vertex, y_vertex)
    """
    assert a != 0,      f'a must be non-zero, got {a}'
    assert decimals >= 0, f'decimals must be >= 0, got {decimals}'
    x_v = -b / (2 * a)
    y_v = a * x_v**2 + b * x_v + c
    return round(x_v, decimals), round(y_v, decimals)

# Using the default (4 decimal places)
x_v, y_v = parabola_vertex(1, -6, 5)
print(f'y = x^2 - 6x + 5:  vertex at ({x_v}, {y_v})')

# Overriding the default to 2 decimal places
x_v, y_v = parabola_vertex(2, 4, 1, 2)
print(f'y = 2x^2 + 4x + 1: vertex at ({x_v}, {y_v})')

x_v, y_v = parabola_vertex(-1, 4, 0, 2)
print(f'y = -x^2 + 4x:     vertex at ({x_v}, {y_v})')
โœ๏ธ Your Task:

Call parabola_vertex(1, -6, 5) and parabola_vertex(1, -6, 5, 2). How does the output differ? Then explain: what is a "default argument" and when is it useful?

Exercise 7C โ€” Function Composition

One function can call another. Here, quadratic_report calls quadratic_eval and classify_roots. Type and run this code:

def quadratic_eval(a, b, c, x):
    """Evaluate y = ax^2 + bx + c at x."""
    return a * x**2 + b * x + c

def classify_roots(a, b, c):
    """Return a string describing the roots of ax^2 + bx + c = 0."""
    assert a != 0, f'a must be non-zero, got {a}'
    d = b**2 - 4 * a * c
    if d > 0:
        return 'Two distinct real roots'
    elif d == 0:
        return 'One repeated real root'
    else:
        return 'No real roots'

def quadratic_report(label, a, b, c):
    """Print a short analysis of y = ax^2 + bx + c."""
    x_v = -b / (2 * a)
    y_v = quadratic_eval(a, b, c, x_v)   # calls quadratic_eval
    print(f'=== {label} ===')
    print(f'  a={a},  b={b},  c={c}')
    print(f'  Vertex: ({x_v:.2f}, {y_v:.2f})')
    print(f'  Roots:  {classify_roots(a, b, c)}')   # calls classify_roots

quadratic_report('Example A', 1, -6, 5)
โœ๏ธ Your Task:

Call quadratic_report for two more quadratics: one with no real roots (try a=1, b=0, c=4) and one with a repeated root (try a=1, b=2, c=1). What changes in the output each time?

0:35โ€“1:10 โ€” Part 2 โ€” Practical Lab: Polynomial Function Library

Work in pairs. Build a set of functions for analyzing quadratic polynomials. Every function must have a docstring. Use assert where inputs could be invalid.

Required Functions

โœ๏ธ Your Task:
Build ALL of these functions with docstrings:

1. poly_eval(a, b, c, x) โ†’ float
   Returns: the value of ax^2 + bx + c at x
   (No assertion needed โ€” any numbers are valid inputs)

2. find_vertex(a, b, c) โ†’ tuple
   Returns: (x_vertex, y_vertex)
   Hint: call poly_eval to compute y_vertex
   Assert: a != 0

3. describe_roots(a, b, c) โ†’ str
   Returns: 'Two real roots'  if b^2 - 4ac > 0
            'One real root'   if b^2 - 4ac == 0
            'No real roots'   if b^2 - 4ac < 0
   Use if/elif/else. Assert: a != 0

4. opens_toward(a) โ†’ str
   Returns: 'upward'   if a > 0
            'downward' if a < 0
   Assert: a != 0

5. print_poly_report(label, a, b, c)
   Prints a report by calling the four functions above.
   Format (print each line separately):
     === label ===
       Coefficients: a=..., b=..., c=...
       Vertex:  (x_v, y_v)
       Opens:   upward / downward
       Roots:   Two real roots / One real root / No real roots

1:10โ€“1:25 โ€” Part 3 โ€” Extension: Bisection Inside a Function

Wrap the bisection algorithm from Week 6 inside a def. The formula for the quadratic is computed inline using variables โ€” no nested functions needed.

Extension

Type and run this code:

def bisect_quad(a, b, c, target, lo, hi, eps=1e-6):
    """
    Find x in [lo, hi] where ax^2 + bx + c = target, using bisection.

    Args:
        a, b, c : Quadratic coefficients
        target  : Target value
        lo, hi  : Search interval endpoints
        eps     : Convergence tolerance (default 1e-6)
    Returns:
        float: x where ax^2 + bx + c is approximately equal to target
    """
    assert lo < hi, f'lo must be less than hi, got lo={lo}, hi={hi}'
    assert eps > 0, f'eps must be positive, got {eps}'

    f_lo = a*lo**2 + b*lo + c - target
    f_hi = a*hi**2 + b*hi + c - target
    assert f_lo * f_hi < 0, f'No sign change in [{lo}, {hi}] โ€” bisection cannot find a root'

    steps = 0
    while abs(hi - lo) > 2 * eps and steps < 200:
        mid   = (lo + hi) / 2
        f_mid = a*mid**2 + b*mid + c - target
        f_lo  = a*lo**2  + b*lo  + c - target
        if f_mid * f_lo < 0:
            hi = mid
        else:
            lo = mid
        steps += 1
    return (lo + hi) / 2

# Find x where x^2 - 6x + 5 = -3  (left root is x = 2)
root = bisect_quad(1, -6, 5, -3, 0.5, 3.0)
print(f'x^2 - 6x + 5 = -3  โ†’  x โ‰ˆ {root:.6f}')
print(f'Verify: {1*root**2 - 6*root + 5:.6f}  (should be -3.000000)')

# Call the function for three different targets
print()
r1 = bisect_quad(1, -6, 5, -3, 0.5, 3.0)   # left root of x^2-6x+8=0, x=2
r2 = bisect_quad(1, -6, 5, -3, 3.0, 4.5)   # right root of x^2-6x+8=0, x=4
r3 = bisect_quad(1, -6, 5,  0, 0.5, 2.0)   # left root of x^2-6x+5=0,  x=1
print(f'target=-3, left root:  x โ‰ˆ {r1:.4f}')
print(f'target=-3, right root: x โ‰ˆ {r2:.4f}')
print(f'target= 0, left root:  x โ‰ˆ {r3:.4f}')
โœ๏ธ Your Task:

Call bisect_quad(1, -6, 5, -5, 0.0, 4.0). Read the AssertionError message. Why does it fail? (Hint: from Week 5, what is the minimum value of xยฒ โˆ’ 6x + 5?)

1:25โ€“1:30 โ€” Wrap-Up & TA Check-Off

Show TA all 5 functions from Part 2. Call print_poly_report('Test-A', 1, -6, 5) and show the output.