At the end of How to use Define Class I hinted there was something interesting about JSL class instances being hard to copy. if X refers to an instance, Y=X makes both Y and X refer to the same instance. The same thing happens when you pass an instance to a user defined function: the function's value is not a copy of the instance; it refers to the same instance. So changing the instance inside the function changes it for the caller as well. If you have ever passed a matrix to a function and modified it in the function, expecting the caller's copy to be updated, you know JSL passes arguments to user functions by value.
JMP passes arguments by making copies, but...
Notice the matrix is copied, so changes to the matrix inside the function are made to the copy, not the original. Notice the big black reference dot is also copied, but not the object it refers to. This causes beginning JAVA programmers many headaches because they have not seen this picture. Within the function, you can do two very different things to variable a.
- you can call a method, like a:set(42);
- you can assign a value, like a=42;
In case (1) you change the argyle object's value from 3 to 42. This is probably what you mean to do. in case (2), you overwrite the big black dot next to a with 42. You probably didn't mean to do that.
The following code uses objects that keep references to other objects. All of the objects have a next() method, so the objects can be composed in interesting ways. The next() method produces the next value in a sequence of values. Some objects represent a constant and always produce the same next value. Others, like the add object, have two other objects (the left side and right side of the + operator). Add:next() calls left:next() and right:next() and adds the two answers.
The complete JSL is attached. Here it is with the pictures interleaved. Let's start with the simplest class.
Define Class(
"vwrap",
m_v = .;
_init_ = Method( {v},
If( Type( v ) == "Class",
Throw( "vwrap is for number or matrix, not class " || Char( v << getname ) )
);
m_v = v;
);
set = Method( {v},
If( Type( v ) == "Class",
Throw( "vwrap is for number or matrix, not class " || Char( v << getname ) )
);
m_v = v;
);
next = Method( {},
m_v;
);
);
Vwrap is the name of the class, and it wraps a value, either a number or a matrix. It has a setter that will reject values it doesn't appreciate and a getter, named next(), to return the next value in a sequence. Because it is a bit ugly to write NewObject(Vwrap(42)), let's also make a helper function to make Vwraps.
v = Function( {x},
If( Type( x ) == "Class",
If( !(x << Contains( "next" )),
Throw( "v expects number, matrix, or one of the classes it supports, not class " || (x << getname) )
);
x;
,
If( !Is Number( x ) & !Is Matrix( x ),
Throw( "v expects number or matrix, not " || Type( x ) )
);
New Object( vwrap( x ) );
)
);
v(...) will get called a lot, sometimes with a class instance, sometimes with a scalar or matrix, and it needs to return the class instance unchanged when it gets one. It might as well check that there is a next() in the class at the same time, just in case there are other classes that might get mixed up.
Next, a largely boiler plate function to make a graph.
gr = Function( {Xvar, Yvar, title, n},
{Xs, Ys, dt},
Yvar = v( Yvar );
Xvar = v( Xvar );
Ys = J( n, 1, Yvar:next() );
Xs = J( n, 1, Xvar:next() );
dt = As Table( Ys || Xs, <<invisible, <<ColumnNames( {"y", "x"} ) );
Eval(
Eval Expr(
dt << Graph Builder(
title( title ),
Show Control Panel( 0 ),
Show Legend( 0 ),
Variables( X( :x ), Y( :y ) ),
Elements( Line( X, Y, Legend( 2 ), Row order( 1 ) ), Points( X, Y, Legend( 3 ) ) ),
SendToReport(
Dispatch( {}, "x", ScaleBox, {Add Ref Line( 0, "Solid", "Black", "", 1 ), Label Row( Show Major Grid( 1 ) )} ),
Dispatch( {}, "y", ScaleBox, {Add Ref Line( 0, "Solid", "Black", "", 1 ), Label Row( Show Major Grid( 1 ) )} ),
Dispatch( {}, title, OutlineBox, {Set Title( "" )} ),
Dispatch( {}, "graph title", TextEditBox, {Set Text( title )} )
),
<<onclose(
Close( Expr( dt ), nosave );
1;
)
)
)
);
);
You can see the calls to v() for the X and Y parameters; those can be simple numbers that will get wrapped, or other classes with a next() method. Then the J() function makes matrices of N values, and a invisible table is made, and Graph Builder runs, with a bit of magic to close the hidden table when it is no longer needed. Let's try it.
gr( 13, 42, "Value 13,42", 1 );
A single point, at x=13, y=42
Hang on, it will get better...Let's make a counter class that will return 0,1,2,3,... on successive calls to next().
Define Class(
"counter",
m_rate = .;
m_pos = empty();
_init_ = Method( {rate},
m_rate = v( rate );
);
next = Method( {},
rate = m_rate:next();
If( Is Empty( m_pos ),
result = 0 * rate;
m_pos = rate;
,
result = m_pos;
m_pos += rate;
);
result;
);
);
The _init_ method takes a rate that determines how fast the counter increments. Rate can be a constant, typically 1, or it can be another class instance. Here's a helper function to make a counter, and a first graph.
ctr = Function( {rate = 1},
New Object( counter( rate ) )
);
gr( ctr(), ctr() , "count up A (equal steps)", 5 );
Notice two separate counter instances were passed to gr(...) for the X and Y axes.
Two counters, running in parallel, for five steps starting at zero.
Now, for something different: let's make the counter's rate be another counter.
gr( ctr(), ctr( ctr() ), "count up B (first step is 0)", 5 );
The first step is zero because the rate counter's first result is zero.
This graph's Y axis is using functional composition of two counters. Lets add a sin() class.
Define Class(
"sin",
m_rate = .;
m_amp = .;
m_pos = .;
_init_ = Method( {rate, amp},
m_rate = v( rate );
m_amp = v( amp );
m_pos = -m_rate:next();
);
next = Method( {},
m_amp:next() :* Sin( m_pos += m_rate:next() )
);
);
Sin produces an infinite sine wave at a frequency and amplitude determined by constants or other class instances. It also keeps an internal position. You can tell from the comments that it might need more thought, but it works for this demo. Here's a single cycle waveform.
gr( ctr(), New Object( Sin( 2 * Pi() / 100, 3 ) ), "sin()", 101 );
Single cycle of sine wave
Similar to the counter with an increment of another counter, a sine wave can have a variable frequency.
gr( ctr(), New Object( Sin( New Object( Sin( .0002, 1 ) ), 3 ) ), "variable frequency", 1000 );
Ramping up the frequency using the first bit of another sine wave
And two different frequency sine waves can be plotted against each other.
gr( New Object( Sin( 4 * Pi() / 99, 3 ) ), New Object( Sin( 6 * Pi() / 99, 3 ) ), "2X3 sin()", 100 );
Simple integer ratios make nice patterns
Another class and helper function; the class adds one or more other classes together and the offset helper adds a constant...or another function, probably...untested.
Define Class(
"add",
m_a = {};
_init_ = Method( {a, b = Empty()},
If( Is Empty( b ),
For( i = 1, i <= N Items( a ), i += 1,
Insert Into( m_a, v( a[i] ) )
),
m_a[1] = v( a );
m_a[2] = v( b );
)
);
next = Method( {},
result = m_a[1]:next();
For( i = 2, i <= N Items( m_a ), i += 1,
result = result + m_a[i]:next()
);
result;
);
);
offset = function({off,x},
newobject(add(off,x))
);
With the offset helper one of the early graphs that had an initial zero step can be reworked to have an initial step of one.
gr( ctr(), ctr( offset(1,ctr( 1 )) ), "count up C (first step is 1)", 5 );
Now all the steps are one bigger than before.
n = 100;
basefreq = 2 * Pi() / n;
freq1 = New Object( Sin( 1 * basefreq, 1 / 1 ) );
freq2 = New Object( Sin( 3 * basefreq, 1 / 3 ) );
freq3 = New Object( Sin( 5 * basefreq, 1 / 5 ) );
freq4 = New Object( Sin( 7 * basefreq, 1 / 7 ) );
sum = New Object( Add( {freq1, freq2, freq3, freq4} ) );
gr( ctr(), sum, "4 Odd Harmonics", n + 1 );
Summing odd harmonics at reduced amplitude approximates a square wave.
n = 200;
basefreq = 2 * Pi() / n;
freqs = {};
For( i = 1, i <= 23, i += 2,
Insert Into( freqs, New Object( Sin( i * basefreq, 1 / i ) ) )
);
gr( ctr(), New Object( Add( freqs ) ), "12 Odd Harmonics", n + 1 );
More harmonics help square it up
Define Class(
"multiply",
m_a = {};
_init_ = Method( {a, b = Empty()},
If( Is Empty( b ),
For( i = 1, i <= N Items( a ), i += 1,
Insert Into( m_a, v( a[i] ) )
),
m_a[1] = v( a );
m_a[2] = v( b );
)
);
next = Method( {},
result = m_a[1]:next();
For( i = 2, i <= N Items( m_a ), i += 1,
result = result :* m_a[i]:next()
);
result;
);
);
n = 1000;
freq1 = New Object( Sin( .1, 1 ) );
freq2 = New Object( Sin( 2 * Pi() / n, 1 ) );
prod = New Object( Multiply( freq1, freq2 ) );
gr( ctr(), prod, "multiply", n + 1 );
Multiply two frequencies; the lower frequency creates the envelope.
n = 1000;
freq2 = New Object( Sin( 2 * Pi() / n, 1 ) );
freq1 = New Object( Sin( .1, freq2 ) );
gr( ctr(), freq1, "amp is sin", n + 1 );
Alternatively, control the amplitude of the higher frequency using the lower frequency.
Finally, make a vwrap holding a matrix of odd harmonic frequencies and make a new class to sum the matrix into a scalar.
n = 200;
mat = v( ((1 :: 49 :: 2) * (2 * Pi() / n)) );
sinmat = New Object( Sin( mat, 1 / (1 :: 49 :: 2) ) );
Define Class(
"matrixSum",
m_mat = .;
_init_ = Method( {mat},
m_mat = v( mat )
);
next = Method( {},
Sum( m_mat:next() )
);
);
gr( ctr(), New Object( matrixSum( sinmat ) ), "25 Odd Harmonics", n + 1 );
It will take a lot more harmonics to really reduce the over-shoot and the ripples.
Good doc: https://www.jmp.com/support/help/en/15.1/index.shtml#page/jmp/classes.shtml
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.