πŸ“ Introduction to Loops with Integrals#

Utility of loops#

Branching causes specific blocks of code to be executed only under certain conditions. The conditions are articulated as logical expressions.

How do you write loops?#

The simplest version of loop is a β€œfor loop”.

for looping_variable in sequence:
    code_block
# List of engineering disciplines offered at Drexel University
engineering_disciplines = ['Biomedical Engineering', 'Chemical Engineering', 'Civil Engineering', 'Electrical Engineering', 'Mechanical Engineering', 'Computer Engineering']

# Simple for loop to print each discipline
for discipline in engineering_disciplines:
    print(f"Drexel University offers a program in {discipline}.")
Drexel University offers a program in Biomedical Engineering.
Drexel University offers a program in Chemical Engineering.
Drexel University offers a program in Civil Engineering.
Drexel University offers a program in Electrical Engineering.
Drexel University offers a program in Mechanical Engineering.
Drexel University offers a program in Computer Engineering.

For Loops with Branching#

# Sample voltage measurements
voltage_measurements = [0.5, 3.3, 5.5, 10.2, 12.0, 15.5, 3.3, 12.0, 2.8, 11.1]

# Safe operating levels
lower_threshold = 3.3
upper_threshold = 12.0

# Initialize lists to hold filtered results
safe_voltages = []
threshold_voltages = []

for voltage in voltage_measurements:
    # Check if voltage is within safe operating levels
    if lower_threshold < voltage < upper_threshold:
        safe_voltages.append(voltage)
    # Check if voltage is exactly at the threshold values
    elif voltage == lower_threshold or voltage == upper_threshold:
        threshold_voltages.append(voltage)

print("Safe operating voltage measurements:", safe_voltages)
print("Voltage measurements at threshold levels:", threshold_voltages)
Safe operating voltage measurements: [5.5, 10.2, 11.1]
Voltage measurements at threshold levels: [3.3, 12.0, 3.3, 12.0]

Loops with Flow Control#

Sometimes it is important to add conditional flow control to the loops. This can be done with continue, pass, and break statements.

Incorporating continue, pass, and break into the previous example can demonstrate their utility in controlling the flow of a for loop. These statements are useful in Python for managing loop execution under specific conditions:

  • continue skips the rest of the code inside the loop for the current iteration and proceeds to the next iteration.

  • pass does nothing; it acts as a placeholder for future code.

  • break exits the loop entirely, skipping any remaining iterations.

Modified Example with continue, pass, and break#

# Sample voltage measurements
voltage_measurements = [0.5, 3.3, 5.5, 10.2, -1,  12.0, 15.5, 3.3, 12.0, 2.8, 11.1, -1]

# Safe operating levels
lower_threshold = 3.3
upper_threshold = 12.0

# Initialize lists to hold filtered results
safe_voltages = []
threshold_voltages = []
critical_voltages = []

for voltage in voltage_measurements:
    # Check for critical condition (e.g., negative voltage)
    if voltage < 0:
        print(f"Critical voltage detected: {voltage}V. Investigation required.")
        critical_voltages.append(voltage)
        break  # Exit loop at first sign of critical condition
    
    if voltage == lower_threshold or voltage == upper_threshold:
        threshold_voltages.append(voltage)
        continue  # Skip to the next iteration
    
    if lower_threshold < voltage < upper_threshold:
        safe_voltages.append(voltage)
    else:
        # Placeholder for future condition handling
        pass  # Do nothing for now

print("Safe operating voltage measurements:", safe_voltages)
print("Voltage measurements at threshold levels:", threshold_voltages)
print("Critical voltage measurements:", critical_voltages)
Critical voltage detected: -1V. Investigation required.
Safe operating voltage measurements: [5.5, 10.2]
Voltage measurements at threshold levels: [3.3]
Critical voltage measurements: [-1]

Explanation#

  • Critical Condition with break: We added a condition at the beginning of the loop to check for critical system failures, exemplified here by negative voltages. If such a condition is detected, a message is printed, the voltage is added to critical_voltages, and break is used to exit the loop immediately. This could represent a situation where processing needs to stop because of a significant error or anomaly.

  • Threshold Check with continue: After handling the critical condition, we check if the voltage is at either threshold value. If so, we add it to threshold_voltages and then use continue to skip the remaining code in the loop, moving directly to the next iteration. This ensures that threshold voltages are not also classified as safe voltages.

  • Placeholder with pass: In the else clause, where we might handle voltages outside the safe operating range but not critical, we use pass as a placeholder. This could be where you’d implement additional logic, such as logging these measurements for further analysis. For now, it does nothing but serves as a placeholder for future code.

Loops with Enumerate#

The enumerate function is a built-in function that returns an iterator of tuples containing indices and values from an iterable. It is often used in loops to keep track of the index of the current item.

# List of signal strength measurements
signal_strengths = [0.1, 0.5, 1.5, 2.5, 1.2, 0.4, 0.9, 2.2, 1.8, 0.2, 0.5]

# Threshold for considering a measurement as a peak
threshold = 2.0

# Loop through the list with enumerate to track index and value
for index, strength in enumerate(signal_strengths):
    if strength > threshold:
        print(f"Signal peak detected at position {index} with strength {strength}")
Signal peak detected at position 3 with strength 2.5
Signal peak detected at position 7 with strength 2.2

Explanation#

  • Enumerate Function: enumerate(signal_strengths) takes the signal_strengths list and returns an enumerate object. This object generates pairs containing the index of each element and the element’s value itself (index, strength).

  • Loop with Index and Value: Inside the loop, index represents the position of the measurement in the list, and strength is the measurement value. This allows us to easily refer to both the measurement’s value and its position.

  • Conditional Check: The if statement checks if the current signal strength (strength) exceeds the defined threshold. If it does, it prints a message indicating that a signal peak was detected, including the position (index) and the strength of the signal.

Loops on Dictionaries#

# Dictionary of construction materials and their properties
construction_materials = {
    'concrete': {'density': 2400, 'compressive_strength': 30, 'cost_per_unit': 70},
    'steel': {'density': 7850, 'compressive_strength': 250, 'cost_per_unit': 250},
    'wood': {'density': 700, 'compressive_strength': 40, 'cost_per_unit': 50},
    'brick': {'density': 2000, 'compressive_strength': 20, 'cost_per_unit': 40}
}

# Loop through the dictionary to analyze materials
for material, properties in construction_materials.items():
    print(f"Analyzing {material}:")
    for property, value in properties.items():
        print(f"  {property}: {value}")

# Identifying materials with compressive strength above a threshold
strength_threshold = 50
print("\nMaterials with compressive strength above 50 MPa:")
for material, properties in construction_materials.items():
    if properties['compressive_strength'] > strength_threshold:
        print(f"- {material}")
Analyzing concrete:
  density: 2400
  compressive_strength: 30
  cost_per_unit: 70
Analyzing steel:
  density: 7850
  compressive_strength: 250
  cost_per_unit: 250
Analyzing wood:
  density: 700
  compressive_strength: 40
  cost_per_unit: 50
Analyzing brick:
  density: 2000
  compressive_strength: 20
  cost_per_unit: 40

Materials with compressive strength above 50 MPa:
- steel

Explanation#

  • Looping Through Dictionary: The for material, properties in construction_materials.items(): loop iterates over the construction_materials dictionary. material is the key (e.g., β€˜concrete’, β€˜steel’), and properties is the value, which is another dictionary containing the properties of each material.

  • Nested Loop for Properties: Inside the first loop, another loop iterates through the properties of each material. This nested loop allows for printing out all properties of each material in a readable format.

  • Conditional Analysis: After printing the details of each material, another loop filters materials based on their compressive strength, identifying those with values above a certain threshold (in this case, 50 MPa). This demonstrates how to apply conditions to selectively process or analyze data within a dictionary.

Loops with Zips#

# Molar masses of compounds in g/mol (for simplicity, assume uniform across mixtures)
molar_masses = [18.01528, 44.01, 28.0134]  # Water (H2O), Carbon Dioxide (CO2), Nitrogen (N2)

# Mass fractions of each compound in three different mixtures
mixture_1_fractions = [0.8, 0.1, 0.1]
mixture_2_fractions = [0.3, 0.6, 0.1]
mixture_3_fractions = [0, 0.4, 0.6]

# List of all mixture fractions for iteration
mixtures_fractions = [mixture_1_fractions, mixture_2_fractions, mixture_3_fractions]

# Calculate and print the average molar mass for each mixture
for mixture in mixtures_fractions:
    average_molar_mass = sum(m * f for m, f in zip(molar_masses, mixture))
    print(f"Average molar mass of mixture: {average_molar_mass:.2f} g/mol")
Average molar mass of mixture: 21.61 g/mol
Average molar mass of mixture: 34.61 g/mol
Average molar mass of mixture: 34.41 g/mol

Explanation#

  • Zip Function: zip(molar_masses, mixture) pairs each molar mass with its corresponding mass fraction in the mixture. This is crucial for performing calculations that depend on corresponding elements from multiple lists.

  • Loop Over Mixtures: The outer loop for mixture in mixtures_fractions: iterates over each list of mass fractions (each representing a different mixture).

  • List Comprehension with Zip: Inside the loop, a list comprehension calculates the product of the molar mass and the mass fraction for each compound in the current mixture. sum(...) then adds these products together to get the average molar mass of the mixture.

  • Print Results: For each mixture, the calculated average molar mass is printed, formatted to two decimal places.

While Loops#

While loops are used to execute a block of code repeatedly as long as a specified condition is true. This is useful when the number of iterations is not known beforehand and depends on certain conditions being met.

  • You must be careful for a while loop to not run indefinitely. This can happen if the condition never becomes false. This is called an infinite loop and can cause the program to become unresponsive.

while logical_expression:
    # code_block is repeated until logical expression is false
    code_block

Example: Estimating the Square Root using Newton’s Method#

Newton’s method, also known as the Newton-Raphson method, is an efficient, iterative approach for finding approximations to the roots (or zeroes) of a real-valued function. This example demonstrates how a while loop can be used to implement Newton’s method for estimating the square root of a number, a basic yet practical application in engineering for tasks such as stress analysis, signal processing, and control systems design.

Newton’s Method for Square Roots#

The Newton-Raphson formula to find the square root of a number x can be simplified as:

\[new\_estimate = \frac{1}{2} \left( estimate + \frac{x}{estimate} \right) \]

where estimate is the current approximation of the square root of x, and new_estimate is the improved approximation.

def estimate_sqrt(target, tolerance=1e-6):
    """
    Estimates the square root of a given number using Newton's method.

    Parameters:
    - target: The number to estimate the square root of.
    - tolerance: The tolerance for the difference between successive estimates.

    Returns:
    - float: The estimated square root of the target.
    """
    estimate = target / 2.0  # Initial estimate

    while True:
        new_estimate = 0.5 * (estimate + target / estimate)
        # Check if the difference between successive estimates is within the tolerance
        if abs(new_estimate - estimate) < tolerance:
            break
        estimate = new_estimate

    return estimate

# Example usage
number = 25
sqrt_estimate = estimate_sqrt(number)
print(f"Estimated square root of {number}: {sqrt_estimate:.6f}")
Estimated square root of 25: 5.000000

Explanation#

  • Initial Estimate: The initial guess for the square root is half the target value, a common starting point for positive numbers.

  • While Loop: The loop continues iterating until the difference between successive estimates of the square root is smaller than a specified tolerance, indicating convergence.

  • Break Condition: The loop exits (break) when the change between the current and new estimate is within the desired tolerance, signifying that further iterations would not significantly improve the approximation.

  • Return Value: Once the loop exits, the function returns the last calculated estimate as the square root of the target number.

Useful for User Interactions#

  • useful when you have a system waiting for a user response – for example, a game waiting for a player to make a move, or a chatbot waiting for a user to input a message. Commonly called callbacks.

import ipywidgets as widgets
from IPython.display import display, clear_output

button = widgets.Button(description="Add")
output = widgets.Output()
display(button, output)

current_value = 0
target_value = 10

def on_button_clicked(b):
    global current_value  # Access the global variable
    with output:
        clear_output(wait=True)  # Clear the previous output
        if current_value < target_value:
            current_value += 1  # Increment the current value
            print(f"Current Value: {current_value}")
        else:
            print("Target reached!")

button.on_click(on_button_clicked)

While Loops with Flow Control Statements#

Real Example: Water Treatment Chemical Dosage Optimization#

Scenario: In a water treatment process, the optimal chemical dosage for treating a batch of water depends on various factors like water quality parameters (e.g., pH, turbidity) and chemical concentration. The goal is to iteratively adjust the chemical dosage based on the water quality outcome, aiming for a target quality level with minimal chemical usage.

The process involves incrementally adjusting the chemical dosage and observing the resulting water quality. If the water quality meets the target criteria, the process stops. If not, the dosage is adjusted based on the observed quality.

Simplified Logic for Dosage Optimization#

Objective: Find the optimal dosage of a treatment chemical to achieve a target turbidity level in water.

Assumptions:

  • Initial chemical dosage is set based on standard practice.

  • Turbidity level is measured after each treatment iteration.

  • The process is limited to a maximum number of iterations to prevent excessive chemical use.

Pseudo-Logic:

  1. Initialize variables: initial dosage, target turbidity, maximum iterations.

  2. Iterate with adjustments until the target turbidity is reached or the maximum iterations are exceeded.

  3. Adjust dosage based on the difference between current and target turbidity levels.

  4. Incorporate break for success, continue for minor adjustments, and error handling for unexpected measurements.

import numpy as np  # For random number generation
import random  # To simulate changes in turbidity

def optimize_chemical_dosage(initial_dosage, target_turbidity, max_iterations=10):
    dosage = initial_dosage
    iteration = 0
    
    while iteration < max_iterations:
        iteration += 1
        # Simulate measuring turbidity after applying the current dosage
        current_turbidity = target_turbidity + (random.random() - 0.5) * 2  # Random fluctuation
        
        print(f"Iteration {iteration}: Current Turbidity = {current_turbidity:.2f}, Dosage = {dosage:.2f}")
        
        # Check if target turbidity is achieved
        if abs(current_turbidity - target_turbidity) <= 0.1:  # Tolerance level
            print("Target turbidity level achieved.")
            break
            
        # Adjust dosage based on the difference from target
        elif current_turbidity > target_turbidity:
            dosage += 0.1  # Increase dosage
        elif current_turbidity < target_turbidity:
            dosage -= 0.1  # Reduce dosage if above target to save chemical
            if dosage < 0:  # Error handling for negative dosage
                print("Error: Dosage calculation went below zero.")
                break
        
        # Check for last iteration without success
        if iteration == max_iterations:
            print("Maximum iterations reached without achieving target turbidity.")
    
    return dosage

random.seed(0)  # Set seed for reproducibility

# Example usage
initial_dosage = 1.0  # Initial chemical dosage in some units
target_turbidity = 1.0  # Target turbidity level
final_dosage = optimize_chemical_dosage(initial_dosage, target_turbidity)
print(f"Final optimized chemical dosage: {final_dosage:.2f}")
Iteration 1: Current Turbidity = 1.69, Dosage = 1.00
Iteration 2: Current Turbidity = 1.52, Dosage = 1.10
Iteration 3: Current Turbidity = 0.84, Dosage = 1.20
Iteration 4: Current Turbidity = 0.52, Dosage = 1.10
Iteration 5: Current Turbidity = 1.02, Dosage = 1.00
Target turbidity level achieved.
Final optimized chemical dosage: 1.00

Explanation#

  • Simulation: The change in turbidity after each treatment is simulated with a random fluctuation around the target to mimic real-world uncertainty.

  • Dosage Adjustment: Dosage is adjusted iteratively based on whether the current turbidity is above or below the target.

  • Flow Control: break exits the loop once the target is met or an error (negative dosage) occurs. The loop also stops if the maximum iterations are reached, indicating a potential problem with the process or the assumptions.

  • Error Handling: Checks for negative dosage values to prevent illogical treatment scenarios.

Using loops in integration#

Many engineering applications require integration, which can be understood as calculating the area under a curve. For example, consider the following:

  • estimating the force of water against a bridge or dam

  • determining the amount of time a reactor should operate for a given chemical to be produced

  • processing audio or electrical signals into meaningful input

  • determining the force required to move an object a given distance

For example, to deposit materials on graphene, one could observe the progress of experiments over time to find the amount of mass deposited to optimize the conditions for deposition.

The Riemann integral is the simplest method of approximating the area under a curve. One simply

  • divides the area under the curve into rectangles of equal width \(h\) and height \(f(x_i)\),

  • calculates the area for each rectangle using \(A_i = hf(x_i)\), and

  • sums the areas of each rectangle between some smaller value of \(x\) called \(a\) and a larger value of \(x\) called \(b\).

Mathematically, this process is expressed as

\[\int_{x_i}^{x_{i+1}} f(x) dx= hf(x_i) + O(h^2).\]