3
\$\begingroup\$

I have made a calculator app to help Bengali high-school students in solving mathematical problems on Newtonian equations of linear motion aka SUVAT equations. I am using python version 3.8.8 and streamlit version 0.80.0.

Originally, I have used Bangla as the user interface language of the web app (Github Repo). But here, I am showing everything in English to get proper review and help:

import streamlit as st
import math

#############
# Functions #
#############

def _ask(variable):
    return st.number_input(f'{variable} :', step=None, format='%f')

############
# Main App #
############

st.markdown("<h1 style='text-align: center; color: #ff7903; font-family: Solaimanlipi'> SUVAT Calculator </h1>", unsafe_allow_html=True)

st.write('This calculator app will help to calculate the variables of the Newtonian equations of linear motion aka SUVAT equations.')

st.write('Select any 3 known-valued variables:')

option_s = st.checkbox('Displacement (s)')
option_u = st.checkbox('Initial Velocity (u)')
option_v = st.checkbox('Final Velocity (v)')
option_a = st.checkbox('Acceleration (a)')
option_t = st.checkbox('Time (t)')
known_variables = option_s + option_u + option_v + option_a + option_t

if known_variables <3:
    st.write('Select at least 3 variables.')
elif known_variables == 3:
   st.write('Enter their values in the same unit system. Accordingly, this calculator will return the values of the remaining 2 variables. ')
elif known_variables >3:
    st.write('Select only 3 variables.')

if (option_s is False and option_u and option_v and option_a is False and option_t):    # ['Initial Velocity (u)', 'Final Velocity (v)', 'Time (t)']
    u = ask('Initial Velocity (u)')
    v = ask('Final Velocity (v)')
    t = ask('Time (t)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    s = \\frac{1}{2}(u+v)t, \\quad a = \\frac{v-u}{t}
    $$      """)
        s = 0.5*(u+v)*t
        try:
            a = (v-u)/t
        except ZeroDivisionError:
            st.write('Here $t=0$ is not allowed.')
            a = None
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Displacement $(s) = \\frac{1}{2}(u+v)t =$ ', s)
        st.write('Acceleration $(a) = \\frac{v-u}{t} =$ ', a)

elif (option_s is False and option_u and option_v and option_a and option_t is False):  # ['Initial Velocity (u)', 'Final Velocity (v)', 'Acceleration (a)']
    u = ask('Initial Velocity (u)')
    v = ask('Final Velocity (v)')
    a = ask('Acceleration (a)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    s = \\frac{v^2-u^2}{2a}, \\quad t = \\frac{v-u}{a}
    $$      """)
        try:
            s = (v*v - u*u)/(2*a)
            t = (v - u)/a
        except ZeroDivisionError:
            st.write('Here $a=0$ is not allowed.')
            s = None
            t = None
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Displacement $(s) = \\frac{v^2-u^2}{2a} =$ ', s)
        st.write('Time $(t) = \\frac{v-u}{a} =$ ', t)

elif (option_s is False and option_u and option_v is False  and option_a and option_t): # ['Initial Velocity (u)', 'Acceleration (a)', 'Time (t)']
    u = ask('Initial Velocity (u)')
    a = ask('Acceleration (a)')
    t = ask('Time (t)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    s = ut + \\frac{1}{2}at^2, \\quad v = u + at
    $$      """)
        s = u*t + 0.5*a*t*t
        v = u + a*t
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Displacement $(s) = ut + \\frac{1}{2}at^2 =$ ', s)
        st.write('Final Velocity $(v) = u + at =$ ', v)

elif (option_s is False and option_u is False and option_v and option_a and option_t):  # ['Final Velocity (v)', 'Acceleration (a)', 'Time (t)']
    v = ask('Final Velocity (v)')
    a = ask('Acceleration (a)')
    t = ask('Time (t)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    s = vt - \\frac{1}{2}at^2, \\quad u= v - at
    $$      """)
        s = v*t - 0.5*a*t*t
        u = v - a*t
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Displacement $(s) = vt - \\frac{1}{2}at^2 =$ ', s)
        st.write('Initial Velocity $(u) =u - at =$ ', u)

elif (option_s and option_u is False and option_v is False and option_a and option_t):  # ['Displacement (s)', 'Acceleration (a)', 'Time (t)']
    s = ask('Displacement (s)')
    a = ask('Acceleration (a)')
    t = ask('Time (t)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    u = \\frac{s}{t} - \\frac{1}{2}at, \\quad v = \\frac{s}{t} + \\frac{1}{2}at
    $$      """)
        try:
            u = (s - 0.5*a*t*t)/t
            v = (s + 0.5*a*t*t)/t
        except ZeroDivisionError:
            st.write('Here $t=0$ is not allowed.')
            u = None
            v = None
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Initial Velocity $(u) = \\frac{s}{t} - \\frac{1}{2}at =$ ', u)
        st.write('Final Velocity $(v) = \\frac{s}{t} + \\frac{1}{2}at =$ ', v)

elif (option_s and option_u is False and option_v and option_a is False and option_t):  # ['Displacement (s)', 'Final Velocity (v)', 'Time (t)']
    s = ask('Displacement (s)')
    v = ask('Final Velocity (v)')
    t = ask('Time (t)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    u = \\frac{2s}{t} - v, \\quad a = \\frac{2(vt-s)}{t^2}
    $$      """)
        try:
            u = (2*s)/t - v
            a = 2*(v*t-s)/(t*t)
        except ZeroDivisionError:
            st.write('Here $t=0$ is not allowed.')
            u = None
            a = None
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Initial Velocity $(u) = \\frac{2s}{t} - v =$ ', u)
        st.write('Acceleration $(a)  = \\frac{2(vt-s)}{t^2} =$ ', a)

elif (option_s and option_u is False and option_v and option_a and option_t is False):  # ['Displacement (s)', 'Final Velocity (v)', 'Acceleration (a)']
    s = ask('Displacement (s)')
    v = ask('Final Velocity (v)')
    a = ask('Acceleration (a)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    u = \\sqrt{v^2 -2as}, \\quad t = \\frac{v}{a} - \\frac{\\sqrt{v^2 - 2as}}{a}
    $$      """)
        try:
            u = math.sqrt(v*v - 2*a*s)
            t = v/a - math.sqrt(v*v - 2*a*s)/a
        except ZeroDivisionError:
            st.write('Here $a=0$ is not allowed.')
            t = None
        except ValueError:
            st.write('Here $v^2 < 2as$ is not allowed.')
            u = None
            t = None
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Initial Velocity $(u) = \\sqrt{v^2 -2as} =$ ', u)
        st.write('Time $(t)= \\frac{v}{a} - \\frac{\\sqrt{v^2 - 2as}}{a} =$ ', t)

elif (option_s and option_u and option_v is False and option_a is False and option_t):  # ['Displacement (s)', 'Initial Velocity (u)', 'Time (t)']
    s = ask('Displacement (s)')
    u = ask('Initial Velocity (u)')
    t = ask('Time (t)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    v = \\frac{2s}{t}-u, \\quad a = \\frac{2(s-ut)}{t^2}
    $$      """)
        try:
            v = (2*s)/t - u
            a = 2*(s-u*t)/(t*t)
        except ZeroDivisionError:
            st.write('Here $t=0$ is not allowed.')
            v = None
            a = None
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Final Velocity $(v) = \\frac{2s}{t}-u =$ ', v)
        st.write('Acceleration $(a)= \\frac{2(s-ut)}{t^2} =$ ', a)

elif (option_s and option_u and option_v is False and option_a and option_t is False):  # ['Displacement (s)', 'Initial Velocity (u)', 'Acceleration (a)']
    s = ask('Displacement (s)')
    u = ask('Initial Velocity (u)')
    a = ask('Acceleration (a)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    v = \\sqrt{u^2 + 2as}, \\quad t = -\\frac{u}{a} + \\frac{\\sqrt{u^2 + 2as}}{a}
    $$      """)
        try:
            v = math.sqrt(u*u + 2*a*s)
            t = -u/a + math.sqrt(u*u +2*a*s)/a
        except ZeroDivisionError:
            st.write('Here $a=0$ is not allowed.')
            t = None
        except ValueError:
            st.write('Here $v^2 < 2as$ is not allowed.')
            v = None
            t = None
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Final Velocity $(v) = \\sqrt{u^2 + 2as} =$ ', v)
        st.write('Time $(t) = -\\frac{u}{a} + \\frac{\\sqrt{u^2 + 2as}}{a} =$ ', t)

elif (option_s and option_u and option_v and option_a is False and option_t is False):  # ['Displacement (s)', 'Initial Velocity (u)', 'Final Velocity (v)']
    s = ask('Displacement (s)')
    u = ask('Initial Velocity (u)')
    v = ask('Final Velocity (v)')
    if st.button('Click here to calculate and check the necessary equations') is True:
        st.write("""        The equations which are used to determine the values:
        $$
    a = \\frac{v^2 - u^2}{2s}, \\quad t = \\frac{2s}{u+v}
    $$      """)
        try:
            a = (v*v - u*u)/(2*s)
        except ZeroDivisionError:
            st.write('Here $s=0$ is not allowed.')
            a = None
        try:
            t = (2*s)/(u+v)
        except ZeroDivisionError:
            st.write('Here $u=v=0$ is not allowed.')
            t = None
        st.write('Inserting your given values in these equations, we get: ')
        st.write('Acceleration $(a) = \\frac{v^2 - u^2}{2s} =$ ', a)
        st.write('Time $(t) = \\frac{2s}{u+v} =$ ', t)

An example IO:

#Input:
Displacement (s) : 120
Initial Velocity (u) : 10
Final Velocity (v) : 50

#Outpt:
Acceleration (a) = 10.0
Time (t) = 4.0

I am a beginner-level coder. As you can see in my code, there are many conditional statements and repetitions of lines. I think defining some functions would help. But I am confused about how to do it. From reviewers here, I am looking for help and advice on how to:

  • Optimize the code
  • Reduce the repetitions
  • Make the code structure more compact and efficient
  • Activate LaTeX ($...$) inside the labels of st.number_imput and st.checkbox for a better look
\$\endgroup\$
3
  • \$\begingroup\$ Hey, can you please explain a bit better what you mean by the LaTeX remark at the end? You seem to have some LaTeX in other places, so what is the "problem" with adding more? \$\endgroup\$
    – RGS
    Apr 14 at 8:24
  • \$\begingroup\$ I wish line numbers were shown in SE code blocks. Check the line: option_s = st.checkbox('Displacement (s)'). I want to use LaTeX inside the labels too so that option_s = st.checkbox('Displacement $(s)$') works and so the symbols look same and beatuiful every where. \$\endgroup\$
    – raf
    Apr 14 at 8:33
  • \$\begingroup\$ In case anyone feels interested, here's the chatroom: chat.stackexchange.com/rooms/123015 \$\endgroup\$
    – raf
    Apr 17 at 7:10
3
\$\begingroup\$

Bundle input calls:

Like you said, some code is repeated. That's fine, you are here to learn :)

The first thing I noticed was a bunch of ask calls in the beginning of many cases of your elif. What I thought was "why not bundle those together?"

You could do this by writing an ask_multiple function that takes a list of prompts and applies ask to each prompt. For example:

def ask_multiple(prompts):
    return [ask(prompt) for prompt in prompts]

Here is an example usage:

# ...
u, v, t = ask_multiple(['Initial Velocity (u)', 'Final Velocity (v)', 'Time (t)'])

You could also wrap this functionality and only have a single ask function. But then, if you only wanted a single prompt it would look odd to write ask(["Prompt here >> "]) so you could still distinguish between a string and a list input:

def _ask(prompt):
    return st.number_input(f'{variable} :', step=None, format='%f')

def ask(prompts):
    if isinstance(prompts, str):
        return _ask(prompts)
    elif isinstance(prompts, (list, tuple)):
        return [_ask(prompt) for prompt in prompts]
    else:
        raise ValueError("Expected a string prompt or a list of prompts.")

Example usages:

u, v, t = ask(['Initial Velocity (u)', 'Final Velocity (v)', 'Time (t)'])
u = ask('Initial velocity (u)')

If you prefer, you could also change your ask function so that it is called like this:

ask('Initial Velocity (u)', 'Final Velocity (v)', 'Time (t)')

instead of

ask(['Initial Velocity (u)', 'Final Velocity (v)', 'Time (t)'])

You could achieve this by using the *args notation:

def ask(*prompts):
    return [_ask(prompt) for prompt in prompts]

Now this ask always returns a list of values. If you want, you can use an if to change this to only return the single value if there's only a single prompt.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you so much for your answers. Do you have anything else in mind to suggest? :) Also, what do you think about the LaTeX label issue I mentioned at the end? \$\endgroup\$
    – raf
    Apr 14 at 8:20
1
\$\begingroup\$

Bundle your options

You seem to have a series of variables that are similarly named and that encode options. Imagine you improve your calculator and it now takes 20 options. Will you have a variable for each one of them?

Probably not!

You can do several things here. The simplest thing is to bundle everything together in a list:

options = [
    st.checkbox('Displacement (s)'),
    st.checkbox('Initial Velocity (u)'),
    st.checkbox('Final Velocity (v)'),
    st.checkbox('Acceleration (a)'),
    st.checkbox('Time (t)'),
]

Now all the options are together. Now comes the twist... you need to index into options to access them, and options[0] or options[3] doesn't look descriptive, so you would want to use an enum (docs) or something similar to give names to the indices.

Another possibility is to save them inside a dictionary, so that the "indexing" is already readable from the get go:

options = {
    's': st.checkbox('Displacement (s)'),
    'u': st.checkbox('Initial Velocity (u)'),
    'v': st.checkbox('Final Velocity (v)'),
    'a': st.checkbox('Acceleration (a)'),
    't': st.checkbox('Time (t)'),
}

This scales slightly better. You can also make the dictionary creation a bit more automatic, which also turns out to be useful if you later change the method to read the options from st.checkbox to something else:

opts = [
    ('s', 'Displacement'),
    ('u', 'Initial Velocity'),
    ('v', 'Final Velocity'),
    ('a', 'Acceleration'),
    ('t', 'Time'),
]
options = {letter: st.checkbox(f"{name} ({letter})") for letter, name in opts}

If things start getting really fancy, you might consider creating a simple class that loads the options, checks if certain combinations are true, etc.

Tied to that, consider adding short helper functions that check for certain conditions, instead of writing all the conditions explicitly in the if and elifs. That is, convert the long/longer sets of conditions to short helper functions with a descriptive name, so that your conditionals are easier to read.

Does this make sense?

\$\endgroup\$
6
  • \$\begingroup\$ Why did you post this as two separate answers? It looks like they can be well combined. \$\endgroup\$
    – Mast
    Apr 13 at 17:36
  • \$\begingroup\$ @Mast not entirely sure how this community works, but the idea I had in my mind is that different people could only address some points, and therefore write answers only about those parts. Splitting concerns also makes it easier for people to agree/disagree with the feedback. E.g. everyone could be on board with what I say about bundling input calls but disagree strongly with the way I suggest the bundling of options, or whatever I suggest on other parts of the OP's code. \$\endgroup\$
    – RGS
    Apr 13 at 17:39
  • \$\begingroup\$ It's not wrong, but we don't see it often nowadays. Considering both answers are about bundling, I'd have thought they made more sense as one answer, together. \$\endgroup\$
    – Mast
    Apr 13 at 17:47
  • \$\begingroup\$ Maybe I was unfortunate in the "naming" of the answers. It's just that the code here is fairly large and after I give it some thought, I'm sure I'll come up with other nice tips :) \$\endgroup\$
    – RGS
    Apr 13 at 17:48
  • 1
    \$\begingroup\$ No problem, both answers make sense. \$\endgroup\$
    – Mast
    Apr 13 at 17:51
1
\$\begingroup\$

Rewrite long conditionals

When you have really long conditionals that depend on checking the state of many different things, your conditionals would improve if you were able to make them more descriptive and/or shorter.

Making them shorter is simpler, and you don't have to do other things suggested in other answers. For example, you could just do the following:

# ask for the option_X inputs

options = [option_s, option_u, option_v, option_a, option_t]

if sum(options) < 3:
    st.write("...")
elif sum(options) == 3:
    st.write("...")
else:
    st.write("...")

if options == [False, True, True, False, True]:   # ['Initial Velocity (u)', 'Final Velocity (v)', 'Time (t)']
    # ...
elif options == [False, True, True, True, False]:    # ['Initial Velocity (u)', 'Final Velocity (v)', 'Acceleration (a)']
    # ...
elif options == [False, True, False, True, True]:     # ['Initial Velocity (u)', 'Acceleration (a)', 'Time (t)']
    # ...

Etcetera. Notice that the conditionals are now much shorter, but not necessarily very easy to understand on their own, so the comments you have help you remember what you are checking.

(By the way, in Python 3.10+ you would probably want to do this with structural pattern matching.)

Another thing you could try is writing helper functions whose names and/or arguments tell you exactly what you are testing.

For example, if you define options this way (a suggestion from another answer):

opts = [
    ('s', 'Displacement'),
    ('u', 'Initial Velocity'),
    ('v', 'Final Velocity'),
    ('a', 'Acceleration'),
    ('t', 'Time'),
]
options = {letter: st.checkbox(f"{name} ({letter})") for letter, name in opts}

then you could write this helper function:

def check_options(options, values):
    for option in options:
        if option in values and not options[option]:
            return False
        elif option not in values and options[option]:
            return False
    return True

This function takes a dictionary options with the options that are set, and a list/tuple with the letters of the options you want to be True. It returns True if all those, and only those, are True.

For example:

check_options(
  {
    's': True,
    'u': False,
  },
  ['s']
) # Returns True
check_options(
  {
    's': True,
    'u': True,
  },
  ['s']
) # Returns False
check_options(
  {
    's': False,
    'u': False,
  },
  ['s']
) # Returns False

I wrote the function in a more verbose way above, it could actually be something like:

def check_options(options, values):
    for option, flag in options.items():
        if flag != (option in values):
            return False
    return True

Take your time to digest it.

With the function, your if and elif become

if check_options(options, ['s', 'u', 'v']):
    # ...
elif check_options(options, ['u', 'v', 'a']):
    # ...

Now the code is self-evident and you know the options you are checking :)

LaTeX

By the way, if you ended up creating the options like I mentioned above, with the dict:

opts = [
    ('s', 'Displacement'),
    ('u', 'Initial Velocity'),
    ('v', 'Final Velocity'),
    ('a', 'Acceleration'),
    ('t', 'Time'),
]
options = {letter: st.checkbox(f"{name} ({letter})") for letter, name in opts}

Then adding LaTeX would be a matter of adding $$ in the f-string:

opts = [
    ('s', 'Displacement'),
    ('u', 'Initial Velocity'),
    ('v', 'Final Velocity'),
    ('a', 'Acceleration'),
    ('t', 'Time'),
]
options = {letter: st.checkbox(f"{name} (${letter}$)") for letter, name in opts}
\$\endgroup\$
7
  • \$\begingroup\$ Thank you so much, At my end, LaTeX is not functioning in the f-string inside st.checkbox. \$\endgroup\$
    – raf
    Apr 14 at 12:02
  • \$\begingroup\$ After options = {letter: st.checkbox(f"{name} ({letter})") for letter, name in opts}, I have to check if len(options) is 3 or not. I was using conditional statements to print statements like st.write('Select at least 3 variables.'). But it's not functioning now. I need to show u my current version of the code. \$\endgroup\$
    – raf
    Apr 14 at 12:14
  • 1
    \$\begingroup\$ @raf to see how many options are True, you can sum the options.values(): tio.run/##K6gsycjPM/7/P7@gJDM/… \$\endgroup\$
    – RGS
    Apr 14 at 12:46
  • \$\begingroup\$ oh I see. So, here's my current situation: gist.github.com/rafisics/0a4753def9334bdfe54ca84f5e17a552 \$\endgroup\$
    – raf
    Apr 14 at 13:09
  • \$\begingroup\$ I am wondering if it's possible to reduce my other repetitions like the same st.button and st.write statements by keeping them in a function! \$\endgroup\$
    – raf
    Apr 14 at 13:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.