Wipy no float and math!

Questions and discussion about The WiPy 1.0 board and CC3200 boards.
Target audience: Users with a WiPy 1.0 or CC3200 board.
User avatar
roncromberge
Posts: 8
Joined: Tue Jan 12, 2016 8:04 pm

Wipy no float and math!

Post by roncromberge » Thu Mar 17, 2016 7:58 pm

Hello,

I have a question. Why have the wipy no float and math lib??? One reason is lack of memory. Sure good answer, But the esp8266 implementation with far less memory has at least float arithmetic build in... >>> 12/2 gives me a proper 6.0 as answer.

Is it in the pipeline to implement float / math in the wipy?

Greetz,

Ron

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Wipy no float and math!

Post by pythoncoder » Fri Mar 18, 2016 5:40 am

Peter Hinch
Index to my micropython libraries.

User avatar
roncromberge
Posts: 8
Joined: Tue Jan 12, 2016 8:04 pm

Re: Wipy no float and math!

Post by roncromberge » Fri Mar 18, 2016 10:55 am

Yes, I've read it. when I plowed through the forum. I am a novice user in both micro-controllers and micro-Python. So I can not judge how much memory takes a minimum float and math implementation. I've seen come across a number of 40kb, but have no idea how many percent of the space it occupies.

Currently a esp8266 board with micro-python is for me the choice to continue. (even a model-01 does the work.) (I'm one of the backers of the esp8266 kickstart). Too bad for my two wipies, that can't do their job without float.

Pity! ;)

Translated from dutch 2 English via Google Translate.

gpshead
Posts: 6
Joined: Tue Nov 03, 2015 5:12 am
Contact:

Re: Wipy no float and math!

Post by gpshead » Fri Apr 08, 2016 5:15 pm

What do you _need_ floating point for?

Reconsider how you implement things for microcontrollers.

Floating point is generally overkill for most all applications. Merely a convenience that desktop computer users have become used to.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Wipy no float and math!

Post by pythoncoder » Fri Apr 08, 2016 7:27 pm

Perhaps a sweeping statement: it rather depends on what you're doing. Anything involving G sensors, magnetometers and gyros leads you into floating point and trigonometry. DSP processor chips have been available with floating point since the late 1980's for one good reason: while you can do DSP in fixed point it needs a great deal of care with regard to scaling.
Peter Hinch
Index to my micropython libraries.

jgmdavies
Posts: 57
Joined: Tue Aug 09, 2016 2:39 pm

Re: Wipy no float and math!

Post by jgmdavies » Fri Aug 26, 2016 3:16 pm

@roncromberge

Hi Ron,

I've written a simple and kludgy WiPy float package that you are welcome to try, including add/subtract/multiply/divide. On my WiPy it takes about 5 Kb as measured by gc.mem_free(), leaving 45 Kb free (the test module takes much more but isn't normally needed).
Warning - there is approximately no error checking. So far it seems to behave reasonably over a useful range, and it's hopefully OK for sensor readings and similar.
Example usage for a hypothetical temperature conversion:

Code: Select all

f1 = Float("299.34")    # e.g. an absolute temperature from somewhere
f2 = Float.subtract(f1, Float(273))   # compute f2 = f1 - 273
print(f2.str() + "deg C")     # "26.34degC"
Please study the tests for more info.

Best regards,
Jim

WiPyFloat.py

Code: Select all

# Float.py
# Last modified: 26 Aug 2016

# J.G.Davies, Jacobus Systems Ltd., Brighton & Hove, UK.
# This code is hereby released as Public Domain.  I would appreciate an acknowledgement if you find it useful.


class Float():

    mantissa = 0
    exponent = 0


    def __init__(self, f=None):
        if f:
            if (type(f).__name__ == "Float"):
                self.mantissa = f.mantissa
                self.exponent = f.exponent
            elif (type(f).__name__ == "int"):
                self.mantissa = int(f)
                self.exponent = 0
            elif (type(f).__name__ == "str"):
                self.load(str(f))

            self.normalise()

        return


    def __eq__(self, other):
        self.normalise()
        other.normalise()
        return (self.mantissa == other.mantissa) and (self.exponent == other.exponent)


    def __str__(self):
        return self.str()


    # Static methods.

    @staticmethod
    def add(f1, f2):
        # Return a new Float holding (f1 + f2).
        result = Float(f1)
        result.add_float(f2)
        return result

    @staticmethod
    def divide(f1, f2):
        # Return a new Float holding (f1 / f2).
        result = Float(f1)
        result.divide_float(f2)
        return result

    @staticmethod
    def multiply(f1, f2):
        # Return a new Float holding (f1 * f2).
        result = Float(f1)
        result.multiply_float(f2)
        return result

    @staticmethod
    def subtract(f1, f2):
        # Return a new Float holding (f1 - f2).
        result = Float(f1)
        result.subtract_float(f2)
        return result


    # Class methods.
    def add_float(self, f, add=True):
        # Add Float 'f' to this Float.

        # Align the exponents.
        self_mantissa = self.mantissa
        self_exponent = self.exponent
        f_mantissa = f.mantissa
        f_exponent = f.exponent

        if (f_exponent < self_exponent):
            shift = self_exponent - f_exponent
            self_mantissa *= 10 ** shift
            self_exponent -= shift
        elif (f_exponent > self_exponent):
            shift = f_exponent - self_exponent
            f_mantissa *= 10 ** shift
            f_exponent -= shift

        if add:
            self.mantissa = self_mantissa + f_mantissa
        else:
            self.mantissa = self_mantissa - f_mantissa

        self.exponent = self_exponent
        self.normalise()

        return self


    def debug(self):
        s = "Float: {0}, {1}".format(self.mantissa, self.exponent)
        return s


    def divide_float(self, f):
        # Divide this Float by 'f'.
        self.mantissa *= 100000000
        self.exponent -= 8

        self.mantissa //= f.mantissa
        self.exponent -= f.exponent
        self.normalise()

        return self


    def int(self):
        result = 0
        return result


    def load(self, s):
        s = s.strip().lower()
        pos = True

        if (s[0] == "+"):
            s = s[1:]
        elif (s[0] == "-"):
            s = s[1:]
            pos = False

        if "e" in s:
            return self.load_scientific(s, pos)

        if "." in s:
            whole, decimal = s.split(".")

            if not decimal:
                # There is no decimal part.
                # TODO: if 'whole' ends in zeros, could scale the number and set self.exponent.
                # This would help keep 'mantissa' a simple 31-bit int on the WiPy.
                self.mantissa = int(whole)
                if not pos: self.mantissa = -self.mantissa
                self.exponent = 0
                self.normalise()
                return True

            # There is a decimal part.
            if whole and (int(whole) == 0):
                whole = None

            if not whole:
                # There is no whole part.
                value, nleading = self.parse_decimal(decimal)
                self.mantissa = int(value)
                if not pos: self.mantissa = -self.mantissa
                self.exponent = -nleading - len(value)
                self.normalise()
                return True

            # There is a whole part and a decimal part.
            whole = whole.lstrip("0")
            decimal = decimal.rstrip('0')
            self.mantissa = int(whole + decimal)
            if not pos: self.mantissa = -self.mantissa
            self.exponent = -len(str(decimal))
            self.normalise()

        else:
            # TODO: if 'whole' ends in zeros, could scale the number and set self.exponent.
            # This would help keep 'mantissa' a simple 31-bit int on the WiPy.
            self.mantissa = int(s)
            if not pos: self.mantissa = -self.mantissa
            self.exponent = 0
            self.normalise()

            return True

        return True


    def multiply_float(self, f):
        # Multiply this Float by 'f'.
        self.mantissa *= f.mantissa
        self.exponent += f.exponent
        self.normalise()

        return self


    def normalise(self):
        # Normalise where possible.
        if (self.mantissa != 0):
            s = str(self.mantissa)
            stripped = s.rstrip("0")
            diff = len(s) - len(stripped)

            if (diff > 0):
                self.mantissa = int(stripped)
                self.exponent += diff

        return True


    def str(self, format="F"):
        if ("E" in format):
            return self.str_scientific()

        # Use format "F".
        pos = (self.mantissa >= 0)
        result = str(abs(self.mantissa))

        if (self.exponent > 0):
            result += "0" * self.exponent
        elif (self.exponent < 0):
            if (-self.exponent < len(result)):
                cut = len(result) + self.exponent
                result = result[:cut] + "." + result[cut:]
            else:
                n_zeros = -self.exponent - len(result)
                result = "0." + ("0" * n_zeros) + result

        if not pos: result = "-" + result

        return result


    def subtract_float(self, f):
        # Subtract Float 'f' from this Float.
        return self.add_float(f, False)



    # Helper methods.

    def load_scientific(self, s, pos):
        mant, exp = s.split("e")
        self.load(mant)
        self.exponent += int(exp)
        self.normalise()

        return False


    def parse_decimal(self, s):
        nleading = 0

        for c in s:
            if (c != "0"): break
            nleading += 1

        if (nleading == 0):
            value = s
        else:
            value = s[nleading:]

        return value, nleading


    def str_scientific(self):
        pos = (self.mantissa >= 0)
        result = str(abs(self.mantissa))
        len_mantissa = len(result)

        if (len(result) > 1):
            result = result[:1] + "." + result[1:]
            result = result.rstrip("0")
            result = result.rstrip(".")

        result += "e" + str(self.exponent + len_mantissa - 1)

        if not pos: result = "-" + result

        return result


#import Float_tests

Float_tests.py

Code: Select all

# Float_tests.py
# Last modified: 26 Aug 2016

# J.G.Davies, Jacobus Systems Ltd., Brighton & Hove, UK.
# This code is hereby released as Public Domain.  I would appreciate an acknowledgement if you find it useful.

from WiPyFloat import Float


def test1():
    # 'load' and 'str' methods.
    print("test1 - load / str")
    f1 = Float()

    f1.load("1.3")
    print(PassFail(f1.str() == "1.3"), "1.3 ->", f1, f1.str("E"))

    f1.load("-1.3")
    print(PassFail(f1.str() == "-1.3"), "-1.3 ->", f1, f1.str("E"))

    f1.load("2000.05")
    print(PassFail(f1.str() == "2000.05"), "2000.05 ->", f1, f1.str("E"))

    f1.load("0.01")
    print(PassFail(f1.str() == "0.01"), "0.01 ->", f1, f1.str("E"))

    f1.load("0.0123")
    print(PassFail(f1.str() == "0.0123"), "0.0123 ->", f1, f1.str("E"))

    f1.load("0.00123")
    print(PassFail(f1.str() == "0.00123"), "0.00123 ->", f1, f1.str("E"))

    f1.load("1.23e-3")
    print(PassFail(f1.str() == "0.00123"), "1.23e-3 ->", f1, f1.str("E"))

    f1.load("1.23e6")
    print(PassFail(f1.str() == "1230000"), "1.23e6 ->", f1, f1.str("E"))

    f1.load("5e6")
    print(PassFail(f1.str() == "5000000"), "5e6 ->", f1, f1.str("E"))

    print("")

    return True


def test2():
    # Addition.
    print("test2 - Addition")
    f1 = Float()
    f2 = Float()

    f1.load("2000.05")
    f2.load("0.001")
    f3 = Float.add(f1, f2)
    print(PassFail(f3.str() == "2000.051"), f1, "+", f2, "->", f3, f3.str("E"))

    f1.load("0.1")
    f2.load("0.2")
    f3 = Float.add(f1, f2)
    print(PassFail(f3.str() == "0.3"), f1, "+", f2, "->", f3, f3.str("E"))

    f1.load("500")
    f2.load("600")
    f3 = Float.add(f1, f2)
    print(PassFail(f3.str() == "1100"), f1, "+", f2, "->", f3, f3.str("E"))

    f1.load("500")
    f2.load("-600")
    f3 = Float.add(f1, f2)
    print(PassFail(f3.str() == "-100"), f1, "+", f2, "->", f3, f3.str("E"))

    f1.load("-500")
    f2.load("600")
    f3 = Float.add(f1, f2)
    print(PassFail(f3.str() == "100"), f1, "+", f2, "->", f3, f3.str("E"))

    print("")

    return True


def test3():
    # Subtraction.
    print("test3 - Subtraction")
    f1 = Float()
    f2 = Float()

    f1.load("2000.05")
    f2.load("0.001")
    f3 = Float.subtract(f1, f2)
    print(PassFail(f3.str() == "2000.049"), f1, "-", f2, "->", f3, f3.str("E"))

    f1.load("0.1")
    f2.load("0.2")
    f3 = Float.subtract(f1, f2)
    print(PassFail(f3.str() == "-0.1"), f1, "-", f2, "->", f3, f3.str("E"))

    f1.load("500")
    f2.load("600")
    f3 = Float.subtract(f1, f2)
    print(PassFail(f3.str() == "-100"), f1, "-", f2, "->", f3, f3.str("E"))

    f1.load("500")
    f2.load("-600")
    f3 = Float.subtract(f1, f2)
    print(PassFail(f3.str() == "1100"), f1, "-", f2, "->", f3, f3.str("E"))

    f1.load("-500")
    f2.load("600")
    f3 = Float.subtract(f1, f2)
    print(PassFail(f3.str() == "-1100"), f1, "-", f2, "->", f3, f3.str("E"))

    print("")

    return True


def test4():
    # Multiplication.
    print("test4 - Multiplication")
    f1 = Float()
    f2 = Float()

    f1.load("5")
    f2.load("6")
    f3 = Float.multiply(f1, f2)
    print(PassFail(f3.str() == "30"), f1, "*", f2, "->", f3, f3.str("E"))

    f1.load("500")
    f2.load("600")
    f3 = Float.multiply(f1, f2)
    print(PassFail(f3.str() == "300000"), f1, "*", f2, "->", f3, f3.str("E"))

    f1.load("5e3")
    f2.load("6e3")
    f3 = Float.multiply(f1, f2)
    print(PassFail(f3.str() == "30000000"), f1, "*", f2, "->", f3, f3.str("E"))

    f1.load("5e6")
    f2.load("6e6")
    f3 = Float.multiply(f1, f2)
    print(PassFail(f3.str() == "30000000000000"), f1, "*", f2, "->", f3, f3.str("E"))

    print("")

    return True


def test5():
    # Division.
    print("test5 - Division")
    f1 = Float()
    f2 = Float()

    f1.load("1")
    f2.load("3")
    f3 = Float.divide(f1, f2)
    print(PassFail(f3.str()[:7] == "0.33333"), f1, "/", f2, "->", f3, f3.str("E"), f3.debug())

    f1.load("5")
    f2.load("6")
    f3 = Float.divide(f1, f2)
    print(PassFail(f3.str()[:7] == "0.83333"), f1, "/", f2, "->", f3, f3.str("E"), f3.debug())

    f1.load("500")
    f2.load("600")
    f3 = Float.divide(f1, f2)
    print(PassFail(f3.str()[:7] == "0.83333"), f1, "/", f2, "->", f3, f3.str("E"), f3.debug())

    f1.load("5e3")
    f2.load("6e3")
    f3 = Float.divide(f1, f2)
    print(PassFail(f3.str()[:7] == "0.83333"), f1, "/", f2, "->", f3, f3.str("E"), f3.debug())

    f1.load("5e6")
    f2.load("6e6")
    f3 = Float.divide(f1, f2)
    print(PassFail(f3.str()[:7] == "0.83333"), f1, "/", f2, "->", f3, f3.str("E"), f3.debug())

    print("")

    return True


def test6():
    # Object creation options.
    print("test6 - Object creation")
    f1 = Float()
    print(PassFail(f1.str() == "0"), f1, f1.debug())

    f1 = Float(123)
    print(PassFail(f1.str() == "123"), f1, f1.debug())

    f1 = Float("3.1416")
    print(PassFail(f1.str() == "3.1416"), f1, f1.debug())

    f2 = Float(f1)
    print(PassFail(f1.str() == "3.1416"), f2, f2.debug())

    print("")

    return True


def test7():
    # Equality / Inequality.
    print("test7 - Equality / Inequality")
    f1 = Float(123)
    f2 = Float("123")
    f3 = Float("123.0001")

    print(PassFail(f1 == f2), "(", f1, "==", f2, ") is", f1 == f2)
    print(PassFail(f1 != f3), "(", f1, "==", f3, ") is", f1 == f3)
    print(PassFail(f1 == f2), "(", f1, "!=", f2, ") is", f1 != f2)
    print(PassFail(f1 != f3), "(", f1, "!=", f3, ") is", f1 != f3)

    print("")

    return True


def PassFail(cond):
    return ("PASS" if cond else "FAIL") + ": "


test1()
test2()
test3()
test4()
test5()
test6()
test7()

jdoege
Posts: 6
Joined: Thu Mar 03, 2016 5:56 pm

Re: Wipy no float and math!

Post by jdoege » Sun Aug 28, 2016 4:47 pm

It bears mentioning that 30 years ago, we accomplished a great deal with 8-bit integer-only operations on small micro controllers so to say you "need" floating point is a bit of an overstatement, though it may make things easier. Everything devolves to integer math under the covers, anyway and, ultimately, you can only get an integer response at some number of significant digits.

The secret to doing things with integer math is to decide how much precision you need (better than 0.5% is achieved with 8 significant bits, 0.1% with 10 bits and so on.) Maintaining n bits of precision across your calculations requires up-scaling for operations and then down-scaling when you are done, mostly for division operations. For instance to maintain 8 bits of integer precision in a divide operation you need to upscale your numerator by 8 bits (multiply by 256), do your integer division with an unscaled denominator, then downscale the answer by 8 bits (integer divide by 256). You must consider your problem space carefully to determine the scaling required (bits of precision and dynamic range of your numbers). Chained operation can introduce compounding rounding errors that require additional bits of precision in the scaling operations. Trigonometric and logarithmic operations require surprisingly small tables of pre-calculated values along with small algorithms to calculate the answer based on table look-ups and mathematical laws (for instance, you only need trig look-ups for 0-45 degrees to get values for an entire circle.)

Properly done, this will be always be faster than software-based floating point and frequently faster than hardware floating point. It also bears mentioning that most people tend to overstate their required precision. Look at all aspects of your control loop and figure out the precision and error for each step. If you are doing anything with a timing loop, for instance, the WiPy seems to only have an error of +/- 500us even though the stated precision is 1us.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Wipy no float and math!

Post by pythoncoder » Mon Aug 29, 2016 8:11 am

OK but some algorithms require floating point. One example is Madgwick sensor fusion used in robotics and drones. Also astronomical calculations such as predicting sun and moon rise and set.

Other algorithms can be performed using integers but with limitations: fast fourier transforms being an example. An integer implementation works for small numbers of frequency bins and low precision, but rounding errors soon accumulate and degrade the results if you need more bins/precision. Error accumulation applies to other filtering algorithms too.

I could go on, but I'm sure you get the picture. I've designed systems based on integer arithmetic, in the days when there was no choice. Guaranteeing that there will never be overflow or underflow under all conditions of inputs can be a serious hassle.

Adding some floating point capability to the WiPy is seriously useful in my opinion.
Peter Hinch
Index to my micropython libraries.

User avatar
kfricke
Posts: 342
Joined: Mon May 05, 2014 9:13 am
Location: Germany

Re: Wipy no float and math!

Post by kfricke » Mon Aug 29, 2016 9:57 am

The existing but empty presence of the WiPy on the Pycom website makes me believe, that it is an abandoned product. :?

And yes adding float support would a nice addition. Counting the number of requests or false assumptions for this, also lets me assume this might be reasonably invested time.
Of course there are reasons against this, but maybe providing an additional build with enabled float emulation and a warning for the trade-off is reasonable.

My use-cases would let me reuse unmodified sensor drivers, which otherwise need to be reworked for fixed-point arithmetic. This is not that of an issue by itself, but this is somewhat wasted work for the probably abandoned WiPy, being the only non-float platform of MicroPython. Simply using the ESP port is a far easier at the moment.

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Wipy no float and math!

Post by Roberthh » Mon Aug 29, 2016 11:20 am

The existing but empty presence of the WiPy on the Pycom website makes me believe, that it is an abandoned product. :?
Maybe it's just that special link. On another spot in this site, it's still there: https://www.pycom.io/solutions/py-boards/wipy/ and available in the web shop. And yes, floating point would be nice, but as discussed at other places, it will need additional RAM space, since, as I've been told by Damien, all code on WiPy is executed in RAM.

Post Reply