• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!

[Snippet] Complex Numbers

Level 23
Joined
Apr 16, 2012
Messages
4,041
Well, the link is that you use this in your resource:

private constant real e = 2.71828182845904523536028747135266249775724700000

This is bad and you shouldn't do that. However, the explanation why it is bad was incorrect. Its not slowing down your resource, but just making the variable to overflow and therefore store an undefined value.

As already mentioned, Wc3 reals are 32 bit floats. This datatype has seven significant digits, which means that if you put in more, it will overflow. Try this:

JASS:
private static method onInit takes nothing returns nothing
	local real e = 2.71828182845904523536028747135266249775724700000		
	call BJDebugMsg(R2S(e*e))
endmethod

For me, it outputs 4.00, while the correct result should be about 7.38.

Thats the reason why I use in my Maths library the constant real E = 2.718282 (seven digits). It is a bit more accurate than the native Jass constant for e, bj_E (six digits), thats why I included that constant in the Maths library. Higher accuracy is not possible with 32 bit floating numbers.

If you want to use a custom constant for e, use that one. Don't use any higher "accuracy", because it will make your variable overflow.

precision is 8, not 7 characters

JASS:
function Trig_Untitled_Trigger_001_Actions takes nothing returns nothing
    local real r = 123456789.0123456789
    call BJDebugMsg(R2S(r))
endfunction

//===========================================================================
function InitTrig_Untitled_Trigger_001 takes nothing returns nothing
    set gg_trg_Untitled_Trigger_001 = CreateTrigger(  )
    call TriggerAddAction( gg_trg_Untitled_Trigger_001, function Trig_Untitled_Trigger_001_Actions )
endfunction

prints 123456784.000
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
precision is 8, not 7 characters

No.


prints 123456784.000

This is not an appropriate test to verify such a thing. Just because the number gets displayed correct, it doesn't mean you really gained anything.

Float has log(2^(23 + 1)) = 7.22 significant bits. The plus one comes from the normalization of the mantissa, from which you can save one extra, the so-called "hidden" bit. However, you don't really have eight significant bits but something between seven and eight. So what does that mean? That means that you don't have enough space to store every number with eight significant bits, but only about 25% of them. However, this is implementation dependent and nobody can guarantee you that you (accidentaly) hit a number which gets stored like this, if this was even implemented at all.

So, what to do in our case which is Warcraft 3? Just lets take a look at a simple example: Compute e^(11):

JASS:
private static method onInit takes nothing returns nothing
	local real e1 = 2.7182818
	local real e2 = 2.718282
			
	call BJDebugMsg(R2S(e1*e1*e1*e1*e1*e1*e1*e1*e1*e1*e1))
	call BJDebugMsg(R2S(e2*e2*e2*e2*e2*e2*e2*e2*e2*e2*e2))
endmethod

Displays:

59874.102
59874.156

while the real value is about 59874.14172. As you can easily see, the second value is closer to the real result, although the constant of e which was used for the calculation has one digit less.

Conclusion: Take the seven digit number ;)
 
Last edited:
Level 14
Joined
Dec 12, 2012
Messages
1,007
Why are you using R2S?... I can't take anything you say seriously when you aren't using R2SW, lol.

Maybe because it doesn't change anything?

If you take a look at the numbers I posted, they have themself already eight significant digits, so I don't need any further "accuracy" by R2SW.

If I use R2SW I get the following output:

59874.101562...
59874.156250...

Does this change anything? I don't think so -.-
The rest of the digits is just useless garbage.

The R2SW function is for displaying digits after the decimal point. If you have sufficient digits before the decimal point (like in this example) it doesn't make sense to use that function at all.
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
Float has log(2^(23 + 1)) = 7.22 significant bits. The plus one comes from the normalization of the mantissa, from which you can save one extra, the so-called "hidden" bit. However, you don't really have eight significant bits but something between seven and eight. So what does that mean? That means that you don't have enough space to store every number with eight significant bits, but only about 25% of them. However, this is implementation dependent and nobody can guarantee you that you (accidentaly) hit a number which gets stored like this, if this was even implemented at all.

nice explanation, I was indeed wrong, but I didnt know the formula exactly, so couldnt really verify, but now I know it :D
 
Level 6
Joined
Jul 30, 2013
Messages
282
I would suggest at this point we have concluded:
Warcraft does NOT treat reals as string... ever. (as was to be expected imho..)

Can we please now stop making such strong claims without reference. how could such absurdities even live this long.. :(
 
Level 6
Joined
Aug 26, 2009
Messages
192
JASS:
static method div takes thistype c1, thistype c2 returns thistype
            local thistype c3 = thistype.allocate()
            local real dv = c2.re*c2.re + c2.im*c2.im
            if (dv!=0) then
                set c3.re = (c1.re*c2.re + c1.im*c2.im)/dv
                set c3.im = (c1.im*c2.re - c1.re*c2.im)/dv
            else
                set c3.re = c1.re*c2.re + c1.im*c2.im
                set c3.im = c1.im*c2.re - c1.re*c2.im
            endif
            return c3
        endmethod

Returning a number when dividing by 0 is somehow... stupid.

JASS:
        method operator arg takes nothing returns real
            return Atan(this.im/this.re)
        endmethod

This is actually wrong. Atan is not enough to calculate the argument. Atan returns negative values, while the argument is defined as the smallest positive number. Then you should also obtain the correct values for the logarithm if you use the prinipal branch.

JASS:
        static method sqrt takes thistype c returns thistype
            local thistype c2 = thistype.allocate()
            if c.im==0 then
                set c2.re = SquareRoot(c.re)
                set c2.im = 0
            else
                set c2.re = SquareRoot((c2.re + SquareRoot(c.re*c.re + c.im*c.im))/2)
                set c2.im = SquareRoot((c2.re - SquareRoot(c.re*c.re + c.im*c.im))/2)
                if c.im < 0 then
                    set c2.im = -c2.im
                endif
            endif
            return c2
        endmethod

This does not make any sense. You use c2.re for calculating the square root of c.re, although c2.re is not initialised yet. I think you wanted to use c.re there.
For the imaginary part, you have to calculate sqrt((abs(c) - c.re)/2 and not c.re - abs(c).
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
Oh sorry for the first one, a mistake of mine.

On the second the argument is the value between ]-pi;pi]
EDIT : Fixed properly

On the third I'll fix the c2 into c my bad.
But I actually got this :
JASS:
set c2.re = SquareRoot((c.re + SquareRoot(c.re*c.re + c.im*c.im))/2)
                set c2.im = SquareRoot((c.re - SquareRoot(c.re*c.re + c.im*c.im))/2)
 
Level 6
Joined
Aug 26, 2009
Messages
192
JASS:
        method operator arg takes nothing returns real
            if this.im>0 then
                return pi/2 - Atan2(this.im, this.re)
            else
                return -pi/2 - Atan2(this.im, this.re)
            endif
        endmethod

I don't understand this. Just go for Atan2(this.im, this.re) and return it. Then you have a range of (-pi,pi], which should be fine, even for the logarithm. (Ignore the stuff i said about it before, just exchange Atan with Atan2.)
 
Level 3
Joined
Jun 25, 2011
Messages
36
Did anyone say something different?

Yes :
static method sqrt takes thistype c returns thistype
* ~ It returns sqrt(c)
* ~ It returns the SquareRoot I choose (because there is actually four squareRoots : two with negative
* imaginary parts and two with positive imaginary parts).
* Basically you'll never have any use of this I think it is more nerdy than anything.
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
@edo : so :
JASS:
Complex c1 = Complex.create()
Complex c2 = Complex.create()
c1.re = 1
c1.im = 1
c2.re = 2
c2.im = 2
c3 = Complex.add(c1, c2)

Is less good than :
JASS:
Complex c1 = Complex.create()
Complex c2 = Complex.create()
c1.re = 1
c1.im = 1
c2.re = 2
c2.im = 2
c3 = c1.add(c2)
??

It is not better.
Struct != Class.
If I could use a C like struct I would have done it long ago.
If there were operators +, -, *, / I would have done this.
But there is not.

@Sapeur -> Wtf was I thinking when I wrote this ?
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
I think Complex.add(c1, c2) is bad, since Complex already stores the member data, so why not use it as member function, but as static function is beyond me.

and I dont know what you mean by Struct != Class, this is both opinion-based and language-based.

and to me you are using this like C-like struct, very much, with the difference that the functions arent free-standing, but are defined as static, which is almost indifferent, but instead of Complex_ you write "Complex.".

What I mean by chainging the API(since you maybe didnt get it) is also that you dont want to invoke some owning member, but you want to take c1, and add c2 to it, and return the Complex value, so Complex.add(c1, c2) is actually very much counter-intuitive
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
It adds nothing and it is not more intuitive...
Or at least I find it bad.
When I made it with non static methods it was like a mess to use, so I chose to put only static method.
If you think putting static method is a shame for OOP I'll change it to function and we'll get an underscore which is even worst.

Struct -> Stores multiple data
Class -> Add functions to manipulate those datas.
You've got both in C++, that's why it is not preference or anything.
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
The only difference between struct and class in C++ is the initial visibility(class private, struct public), so no your description is not fitting C++ at least.

And no I dont say to change them to functions, after all, you are the creator, not me, so they will be as you will make them.
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
You can't add methods for struct you have to define them as functions.
There is no constructor for struct.
There is not destructor for struct.
So basically yes it fits C++.
Now I think we should stop discussing about that anymore on this thread there is already a lot of off-topic.

And to bring back the API subject I prefer it this way and I'll keep it this way, and if people wants the API to be non static, go gy this...
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
I like that this is a quite small library with most of the functionality needed.

However, I think the design could be improved. At the moment it requires quite some code to do basic things. Say I want to compute c3*(c1 + c2) and output it as string, I have to do this:

JASS:
local Complex c1 = Complex.create()
local Complex c2 = Complex.create()
local Complex c3 = Complex.create()
set c1.re = 1
set c1.im = -3
set c2.re = 1.5
set c2.im = -4
set c3.re = 2
set c3.im = 1
call BJDebugMsg(Complex.mul(c3, Complex.add(c1, c2)).toString())

Now, with instance methods and constructor arguments:

JASS:
local Complex c1 = Complex.create(1, -3)
local Complex c2 = Complex.create(1.5, -4)
local Complex c3 = Complex.create(2, 1)
call BJDebugMsg(c3.mul(c1.add(c2)).toString())

Cleaner, easier to read and to understand, right? As a Complex number has to be initialized anyways, it makes sense to restrict construction to explicitly initialized types.

An instance makes always sense if the object you want to model carries a state. A File has state (location within the file system, size, security attributes etc.) and is therefore typically modelled as an "instance" (== non-static) class. A Math function like Sine or Cosine has no state, because it works without any additional data and is therefore a good candidate for a static class or a free function, which typically also doesn't have any internal state.

A complex number has a state, namely its real and imaginary part. Therefore it makes sense to design it as a non-static class that uses instance methods. The computation in the code above also shows that this makes sense from a coding point of view.

And since you already do have instance methods like toString or abs, it would definitly make sense to adopt that to the other methods as well.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Okay updated.

So, could you give a rationale why the resource still uses static methods over instance methods, when instance methods provide a shorter and more readable syntax?

Especially since you use some instance methods, this is not really consistent at the moment. Why are some methods static and some (like toString) not?

Static methods make sense if you can perform an action associated with its class without an actual object of the class itself. You can't perform an addition or subtraction of two complex values without having two complex objects in the first place. Therefore either instance methods or free functions would be more appropriate. However, since we have no function/method overloading in vJass, instance methods are preferable.

Btw, is there a specific reason why the default instance limit of this is 10000? Its fine to provide a constant to modify this value, but I think the default value should be the vJass default value of 8190, since this is most likely sufficient anyways.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Alright, you made a small typo in the constructor, therefore it does not compile at the moment:

static method create takes real r, real i return thistype

->

static method create takes real r, real i returns /* <- */ thistype


EDIT:

It seems there is something wrong with the sqrt implementation... This code

JASS:
local Complex c = Complex.create(4, -1.5)
call BJDebugMsg(Complex.sqrt(c).toString()) // Output: 2.034 - 1.374i

outputs 2.034 - 1.374i, while the correct result is 2.034 - 0.369i. You might want to take a look at this.
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
Oups.
Thanks for pointing it.
I'll check it tomorrow thanks for it.

EDIT : Fixed.

I can't really fix my mind on static and non static.
Okay .add .sub .mul .div sounds good.
But what about .conj ? Static/Non static ? Returns or intern ?
I can't really think of a way to unify it nicely.
 
Last edited:
Level 14
Joined
Dec 12, 2012
Messages
1,007
I can't really fix my mind on static and non static.
Okay .add .sub .mul .div sounds good.
But what about .conj ? Static/Non static ? Returns or intern ?
I can't really think of a way to unify it nicely.

Well, it was more a recommendation than a requirement.

If you really don't like the way with instance methods, its ok too. Its just that I prefer a short syntax, especially since vJass is already quite verbose. And repeating Complex. for every operation increases verbosity.

However, thats just my own opinion, its not a neccessary thing to change.

If you do change it, it would be good if its consistent. There is nothing wrong with making all methods instance methods, .conj is not really different than the other methods (you can only compute the conjugate of a complex number if you already have one, so an instance is required anyways).
 
Top