Problem 6.13 - Torsional Deformation

A system of cylinders are attached to the wall at one end. The first torque is applied at the connection of the cylidner attached to the wall made of steel with the cylinder made of aluminum. The second torque is applied at the connection between the cylinder made of aluminum and the second cylinder made of steel. The third torque is applied at the free end. The first cyliner has length 10 cm and diamter 5 cm. The second cyliner has length 15 cm and diamter  3 cm. The third cyliner has length 8 cm and diamter 4 cm. [Problem adapted from © Kurt Gramoll CC BY NC-SA 4.0]

#| standalone: true
#| viewerHeight: 600
#| components: [viewer]

from shiny import App, render, ui, reactive
import random
import asyncio
import io
import math
import string
from datetime import datetime
from pathlib import Path

def generate_random_letters(length):
    # Generate a random string of letters of specified length
    return ''.join(random.choice(string.ascii_lowercase) for _ in range(length)) 

problem_ID="269"
T1=reactive.Value("__")
T2=reactive.Value("__")
T3=reactive.Value("__")
Gs=77
Ga=27

attempts=["Timestamp,Attempt,Answer,Feedback\n"]

app_ui = ui.page_fluid(
    ui.markdown("**Please enter your ID number from your instructor and click to generate your problem**"),
    ui.input_text("ID","", placeholder="Enter ID Number Here"),
    ui.input_action_button("generate_problem", "Generate Problem", class_="btn-primary"),
    ui.markdown("**Problem Statement**"),
    ui.output_ui("ui_problem_statement"),
    ui.input_text("answer","Your Answer in units of degrees", placeholder="Please enter your answer"),
    ui.input_action_button("submit", "Submit Answer", class_="btn-primary"),
    ui.download_button("download", "Download File to Submit", class_="btn-success"),
)


def server(input, output, session):
    # Initialize a counter for attempts
    attempt_counter = reactive.Value(0)

    @output
    @render.ui
    def ui_problem_statement():
        return[ui.markdown(f"Three moments are applied to the system of cylinders as shown. Assume T<sub>1</sub> = {T1()} kN-m, T<sub>2</sub> = {T2()} kN-m, and T<sub>3</sub> = {T3()} kN-m. If G<sub>steel</sub> = 77 GPa and G<sub>aluminum</sub> = 27 GPa, determine the total angle of twist at the free end.")]
    
    @reactive.Effect
    @reactive.event(input.generate_problem)
    def randomize_vars():
        random.seed(input.ID())
        T1.set(random.randrange(20, 100, 1)/10)
        T2.set(T1()+random.randrange(20, 100, 1)/10)
        T3.set(round(T2()*random.randrange(5, 8, 1)/10, 2))
        
    @reactive.Effect
    @reactive.event(input.submit)
    def _():
        attempt_counter.set(attempt_counter() + 1)  # Increment the attempt counter on each submission.
        F1 = T1()+T2()-T3()
        F2 = T2()-T3()
        F3 = -T3()
        J1 = (math.pi/2)*((5/200)**4)
        J2 = (math.pi/2)*((3/200)**4)
        J3 = (math.pi/2)*((4/200)**4)
        instr= ((F1*1000*.1)/(J1*Gs*10**9) + (F2*1000*.15)/(J2*Ga*10**9) + (F3*1000*.08)/(J3*Gs*10**9))*180/math.pi
        if math.isclose(float(input.answer()), instr, rel_tol=0.01):
            check = "*Correct*"
            correct_indicator = "JL"
        else:
            check = "*Not Correct.*"
            correct_indicator = "JG"

        # Generate random parts for the encoded attempt.
        random_start = generate_random_letters(4)
        random_middle = generate_random_letters(4)
        random_end = generate_random_letters(4)
        encoded_attempt = f"{random_start}{problem_ID}-{random_middle}{attempt_counter()}{correct_indicator}-{random_end}{input.ID()}"

        # Store the most recent encoded attempt in a reactive value so it persists across submissions
        session.encoded_attempt = reactive.Value(encoded_attempt)

        # Append the attempt data to the attempts list without the encoded attempt
        attempts.append(f"{datetime.now()}, {attempt_counter()}, {input.answer()}, {check}\n")

        # Show feedback to the user.
        feedback = ui.markdown(f"Your answer of {input.answer()} is {check}.")
        m = ui.modal(
            feedback,
            title="Feedback",
            easy_close=True
        )
        ui.modal_show(m)

    @session.download(filename=lambda: f"Problem_Log-{problem_ID}-{input.ID()}.csv")
    async def download():
        # Start the CSV with the encoded attempt (without label)
        final_encoded = session.encoded_attempt() if session.encoded_attempt is not None else "No attempts"
        yield f"{final_encoded}\n\n"
        
        # Write the header for the remaining CSV data once
        yield "Timestamp,Attempt,Answer,Feedback\n"
        
        # Write the attempts data, ensure that the header from the attempts list is not written again
        for attempt in attempts[1:]:  # Skip the first element which is the header
            await asyncio.sleep(0.25)  # This delay may not be necessary; adjust as needed
            yield attempt

# App installation
app = App(app_ui, server)