Python Forum
Improvements for first script?
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Improvements for first script?
#1
My way of learning is to think of something I need doing, then try a line, step by step, googling each error message and so on. I *don't* look for a complete script, that someone else has already done, to modify. I know it's re-inventing the wheel, but it works for me :)

Anyway, this is my first ever python script. It very simple, but it works. But, I'm just wondering if it is OK as it stands, or could it be a bit more elegant? I mean, would a grizzled old Python hack give a gentle nod of his head and say something like "Not bad, not bad. For a hardware engineer", or would he curl up in the corner, realising 2020 just got worse?...

EDIT: I know it needs things like making sure you can only input numeric values where they're required.

# Convert millivolts to engineering units
print ("4-20 mA to Engineering units convertor")
print ("")

def main():
    # Set engineering units and range
    measUnits = input("Enter the units of measurement: ")
    measMin = input("Enter the minimum measurement value: ")
    measMin2 = abs(float(measMin)) # removes "minus" bit
    measMax = input("Enter the maximum measurement value: ")
    measRange = float(measMin2) + float(measMax)
    print ("Measurement range is {} {}".format(measRange, measUnits))

    # Input measured value
    rawValue = input("Input mV value: ")

    # Calculate measured value
    step1 = float(rawValue) - 400
    step2 = step1 / 1600
    measValue = (step2 * measRange) - measMin2
    output = round(measValue,2)
    print ("Measured value is {} {}".format(output, measUnits))

main()

# Restart? yay or nay?
restart = input("Do you want to convert another value? Y/N: ")
if restart.lower() == 'y':
    main()
else:
    print ("Goodbye")
Reply
#2
Addressing style part of code:

I think that it would be good idea to read and implement PEP 8 -- Style Guide for Python Code, specifically 'Function and Variable names':

Quote:Function names should be lowercase, with words separated by underscores as necessary to improve readability.

Variable names follow the same convention as function names.

mixedCase is allowed only in contexts where that's already the prevailing style (e.g. threading.py), to retain backwards compatibility.

I would remove meas part from majority of variable names as it is not needed (but look for not clashing with built-in functions like min, max, range).

I would use f-strings instead of .format:

>>> name = 'Bob'
>>> occupation = 'The Builder'
>>> f'Hello {name} {occupation}'
'Hello Bob The Builder'
There is no need for empty space after print. It will not raise error, but it's not a good style.
I'm not 'in'-sane. Indeed, I am so far 'out' of sane that you appear a tiny blip on the distant coast of sanity. Bucky Katt, Get Fuzzy

Da Bishop: There's a dead bishop on the landing. I don't know who keeps bringing them in here. ....but society is to blame.
Reply
#3
Thanks for the tips. Thumbs Up Didn't know about the style guide - Heading over to there ASAP.

For not using uppercase letters, then, measUnits would become meas_units.

I appreciate your comment about not needing to use "meas" everywhere, but I have reading issues and it helps me follow what's going on :)

I will also check out "f-strings". Are they related to g-strings Tongue

EDIT - updated script:

# Convert millivolts to engineering units
print("4-20 mA to Engineering units convertor")
print("")

def main():
    # Set engineering units and range
    meas_units = input("Enter the units of measurement: ")

    # Input minimum value of measurement range
    while True:
        try:
            meas_min = float(input("Enter the minimum measurement value: "))
        except ValueError:
            print("Must be a numeric value...")
            continue
        else:
            break
    
    meas_min2 = abs(float(meas_min)) # removes "minus" bit

    # Input maximum value of measurement range
    while True:
        try:
            meas_max = float(input("Enter the maximum measurement value: "))
        except ValueError:
            print("Must be a numeric value...")
            continue
        else:
            break

    # Display measurement range
    meas_range = float(meas_min2) + float(meas_max)
    print(f"Measurement range is {meas_min} to {meas_max} {meas_units}")
    
    # Input measured value
    while True:
        try:
            raw_value = float(input("Input mV value: "))
        except ValueError:
            print("Must be a numeric value...")
            continue
        else:
            break

    # Calculate measured value
    step1 = float(raw_value) - 400
    step2 = step1 / 1600
    meas_value = (step2 * meas_range) - meas_min2
    output = round(meas_value,2)
    print("Measured value is {} {}".format(output, meas_units))

    restart = input("Do you want to convert another value? Y/N: ")
    print("")
    if restart.lower() == 'y':
        main()
    else:
        print("Goodbye")

main()
Reply
#4
Some observations.

There is the repetition of input validation. If such pattern emerges then it is clear that function should be used. Currently the code is not DRY (Don't Repeat Yourself). Simple validation function should be used, something along those lines:

def validate(request):
    while True:
        answer = input(request)
        try:
            return abs(float(answer))
        except ValueError:
            print(f'Expected numeric value but {answer!r} was entered')
Now you can use this or similar function when you need to validate user input to be numeric and absolute value. You can be 'clever' and use loop and unpacking while using it:

low, high = (validate(f'Enter the {boundary} measurement value: ') for boundary in ['minimum', 'maximum'])
alloydog likes this post
I'm not 'in'-sane. Indeed, I am so far 'out' of sane that you appear a tiny blip on the distant coast of sanity. Bucky Katt, Get Fuzzy

Da Bishop: There's a dead bishop on the landing. I don't know who keeps bringing them in here. ....but society is to blame.
Reply
#5
I will give that a bash - always good to reduce typing ;)

Thanks!
Reply
#6
If I see PLC Content, I can't resist...

Just as additional information:

You can have different measurement ranges.
The most used ranges I know:

Current:
  • 0 - 20 mA [no cable break detection]
  • 4 - 20 mA

Voltage:
  • 0 - 10 V
  • -10 V - +10V # also negative Voltages are possible


The namedtuple is very useful.
from collections import namedtuple


def select_range():
    Range = namedtuple("range", "text low high unit")
    # just to have dot access
    ranges = [
        Range("0 - 10 V", 0, 10, "V"),
        Range("-10 - +10 V", -10, 10, "V"),
        Range("0 - 20 mA", 0, 20, "mA"),
        Range("4 - 20 mA", 4, 20, "mA"),
    ]
    while True:
        for idx, key in enumerate(ranges, 1):
            print(f"{idx}) {key.text}")
        print()
        selector = input("Please select a range: ")
        try:
            # two possible Exceptions
            # ValueError if the conversion to an int is not possible
            # IndexError if you selected a not existing index
            return ranges[int(selector) - 1]
        except (ValueError, IndexError):
            # catching this two exceptions to continue the while True loop
            # and ask again
            print("Invalid input")
I named the namedtuple Range, which is not a good name. It has nothing to do with range().


If the input was valid, the return value is a namedtuple.
You can access the values with dot.:
my_range = select_range()
print(my_range.text)
low, high = my_range.low, my_range.high
unit = my_range.unit
And to normalize and scale, I use these functions:
def scale(value, low, high):
    """
    Scale the value from low .. high

    >>> scale(0, -10, 10)
    -10

    >>> scale(1, -10, 10)
    10
    """
    return value * (high - low) + low


def normalize(value, low, high):
    """
    Normalize value to 0 .. 1

    >>> normalize(0, 0, 10)
    0.0

    >>> normalize(10, 0, 10)
    1.0

    >>> normalize(5, 0, 10)
    0.5
    """
    return (value - low) / (high - low)
Example with selected range:
# normalize 0 .. 27648 to 0 .. 1
# scale 0 .. 1 to 0 .. 100
percent = scale(normalize(15000, 0, 27648), 0, 100)
The thing is, if you know the minimum range and maximum range of the detected physical quantity, the step to convert it to a current or voltage, is not required. Only on PLC-Side the right range must be selected and the right connections must be used.
Example: https://cdn.sick.com/media/pdf/7/47/747/dataSheet_UC30-214163_6054712_en.pdf

The operating range is: 350 mm ... 3400 mm

The scaling with a Siemens PLC will look like this:
input_value = 1337
normalized_value = normalize(input_value, 0, 27648) # 0 - 10 V range
# the 27648 is the highest value of Rated Range
scaled = scale(normalized_value, 350, 3400)
In real world applications you can get also values, which are out of normal range.
For example, the sensor could output a higher current/voltage than 10V/20mA to signalling the PLC that something is not in operation range. If the range is 4 - 20 mA, then you can also detect a broken cable. In this case, the current is 0 mA.

SIEMENS has defined these areas for example:
  • Underflow, at zero voltage and current
  • Undershoot range
  • Rated range
  • Overshoot range
  • Overflow, off power
Source: https://support.industry.siemens.com/cs/...8&lc=en-DE

I've also made for this a function (but without the units):
def scale_siemens(value, min_scale, max_scale, bipolar=False):
    """
    Scale Analog Values with siemens components:
    https://support.industry.siemens.com/cs/mdm/8859629?c=23218577931&t=1&s=27648&lc=en-DE
    Look at Table: Representation of analog values in the ±10 V output range
    """
    if bipolar:
        normalized = normalize(value, -27648, 27648)
    else:
        normalized = normalize(value, 0, 27648)
    ranges = (-32513, -27649, 27648, 32511, 32767)
    areas = [
        "Underflow, at zero voltage and current",
        "Undershoot range",
        "Rated range",
        "Overshoot range",
        "Overflow, off power",
    ]
    idx = bisect.bisect_left(ranges, value)
    try:
        kind = areas[idx]
    except IndexError:
        kind = areas[-1]
    if idx == 0 or idx + 1 >= len(ranges):
        return 0.0, kind
    return scale(normalized, min_scale, max_scale), kind
The interesting part here is the use of the bisect module.
Otherwise, you've a pattern like:
value = 32760 # overshoot range

# From High to low...

if 32512 <= value <= 32767:
    kind = "Overflow, off power"
elif 27649 <= value <= 32511:
    kind = "Overshoot range"
elif -27648 <= value <= 27648:
    kind = "Rated range"
elif -32512 <= value <= -27649:
    kind = "Undershoot range"
elif -32768 <= value <= -32513:
    kind = "Underflow, at zero voltage and current" 

print(kind)
alloydog likes this post
Almost dead, but too lazy to die: https://sourceserver.info
All humans together. We don't need politicians!
Reply
#7
Thanks for extras! Thumbs Up

I'll have a look through them, as I'm thinking about expanding my script later on - it was written as "My First Python Script", as earlier I did the same thing in HTML and javascript at work. We sometimes do quick tests on 4-20mA output sensors by strapping a 100 ohm resistor across them and measuring the voltage drop.
DeaD_EyE likes this post
Reply
#8
(Dec-28-2020, 05:25 AM)perfringo Wrote: I would use f-strings instead of .format:

Is that just a preference or something more?
DeaD_EyE and alloydog like this post
Reply
#9
Apparently f-strings are the new improved way of doing it...

https://realpython.com/python-f-strings/
Reply
#10
(Jan-01-2021, 01:45 PM)alloydog Wrote: Apparently f-strings are the new improved way of doing it...

https://realpython.com/python-f-strings/

In addition to comprehensive information in this very good coverage of f-strings I would add feature what is available from Python <= 3.8: f-strings support = for self-documenting expressions and debugging.

>>> spam = 'Life, The Universe and Everything'
>>> print(f'{spam=}')
spam='Life, The Universe and Everything'
alloydog likes this post
I'm not 'in'-sane. Indeed, I am so far 'out' of sane that you appear a tiny blip on the distant coast of sanity. Bucky Katt, Get Fuzzy

Da Bishop: There's a dead bishop on the landing. I don't know who keeps bringing them in here. ....but society is to blame.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Any improvements Chuck_Norwich 2 2,513 Jul-14-2020, 05:15 AM
Last Post: Gamo
  [split] Any improvements Adamstiffman 1 1,826 May-31-2020, 06:16 AM
Last Post: pyzyx3qwerty
  Coding improvements chris_drak 2 30,471 May-02-2020, 11:39 AM
Last Post: chris_drak

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020