NB: This text may be out-of-date. The latest version is always avalable in CVS as PyApps/test/tdl_tutorial.tdl (or use
this link).
1 # A TDL tutorial
2 # NB: work in progress!!!
3 # This script is meant as both a tutorial and a test of most of the
4 # features of TDL. If something is missing, please feel free to add.
5 # The CVS path is Timba/PyApps/test/tdl_tutorial.tdl.
6
7 # standard preamble, every TDL script must start with this
8 from Timba.TDL import *
9
10 # Initial forest state may be modified by assigning to
11 # Timba.TDL.Settings.forest_state. Since we import all names from Timba.TDL
12 # above, the record is available as Settings.forest_state
13 Settings.forest_state.cache_policy = 100;
14
15 # If False (the default), orphan nodes not assigned to anything (such as a
16 # root group, e.g. ns.ROOT) will be deleted.
17 # If True, orphan nodes are always retained. This is what we want here.
18 Settings.orphans_are_roots = True;
19
20
21 def define_forest (ns):
22 """define_forest() is a standard TDL name. When a forest script is
23 loaded by, e.g., the browser, this method is automatically called to
24 define the forest. The 'ns' argument is a NodeScope object in which
25 the forest is to be defined, usually this is simply the global scope.
26 """;
27 # So, this is where we start defining our forest
28
29 # A node can be defined as follows:
30 ns.a << Meq.Parm(cache_policy=10);
31 # This creates a MeqParm named 'a'. The thingy on the left ('ns.a') is
32 # called a "node stub" (hence 'ns' can be thought of both as "node scope"
33 # and "node stub"), and it determines the name of the node. The thingy
34 # on the left ('Meq.Parm()') is a "node def", which determines the class
35 # of the node and its init-record. The init-record can be given as keyword
36 # parameters to Meq.Parm(). The "<<" operator binds the two together and
37 # results in a real node being created.
38
39 # Other variations on this theme. Here's nodes with a default init-record:
40 ns.a1 << Meq.Parm();
41 ns.a2 << Meq.Parm; # this works too
42 ns.a3 = Meq.Parm;
43 # assignment works too but note the following: the result of operator <<
44 # is the node stub itself, and it that may be reused (see below).
45 # By contrast, Python won't let you reuse the result of operator =
46 # (i.e. you can say a=b=1 but not a=(b=1)+1.) The importance of this
47 # will become obvious below.
48
49 # node names can also have arbitrary qualifiers:
50 ns.b1(1,2,'x',y=2) << Meq.Parm; # creates node named 'b1:1:2:x:y=2'
51
52 # a name may also be computed, the [] operator comes in handy for this
53 name = 'b2';
54 ns[name](1,2) << Meq.Parm; # creates 'b2:1:2'
55
56 # in fact, even the class itself may be computed:
57 classname = 'Parm';
58 ns.b3 << Meq[classname](cache_policy=10);
59
60 # note also that a node def may be reused to create multiple nodes.
61 # Suppose we want to create a bunch of MeqParms, each with a 2x1 polc
62 # in it, with c00=1 and c01=0 We can save the def into a variable:
63 P22 = Meq.Parm([1,0]);
64 # and reuse it several times:
65 ns.b4 << P22;
66 ns.b5 << P22;
67 ns.b6 << P22;
68
69 # Qualifiers are particularly useful when creating whole series of nodes
70 # in a loop:
71 stations = range(1,5);
72 P0 = Meq.Parm(0); # saved def
73 for st in stations:
74 x0 = ns.x0(st) << P0;
75 y0 = ns.y0(st) << P0;
76 z0 = ns.z0(st) << P0;
77 ns.xyz0(st) << Meq.Composer(x0,y0,z0);
78 # This loop creates a set of subtrees (i=1..4) of the form:
79 # xyz0:i (MeqComposer)
80 # +-- x0:i (MeqParm)
81 # +-- y0:i (MeqParm)
82 # +-- z0:i (MeqParm)
83 # So, children to a node may be specified by passing node stubs as
84 # arguments to its definition. (Note that stubs can be assigned to variables
85 # like any other Python object). But, remember that operator <<
86 # returns the node stub. So, we can use it in-place to create the whole
87 # subtree with a single statement:
88 for st in stations:
89 ns.xyz1(st) << Meq.Composer(ns.x1(st)<<P0,ns.y1(st)<<P0,ns.z1(st)<<P0);
90 # or here's an even more concise way to do the same subtree, using Python
91 # list comprehension syntax, and passing in the argument list via *:
92 coords = 'xyz';
93 for st in stations:
94 ns.xyz(st) << Meq.Composer(*[ns[a](st) << P0 for a in coords]);
95
96 # Now, can you guess what tree this going to generate?
97 ns.d1 << Meq.Composer(1,-2,5);
98 # The answer is, also a subtree:
99 # d1 (MeqComposer)
100 # +-- c1 (MeqConstant value=1)
101 # +-- c-2 (MeqConstant value=-2)
102 # +-- c5 (MeqConstant value=5)
103 # So, using a constant where a node is expected implicitly creates a
104 # Meq.Constant node. And here too:
105 ns.d2 << 0; # d2 is a MeqConstant with value=0
106
107 # Of course, children may be nested to any depth. Here's another subtree:
108 ns.solver << Meq.Solver(
109 num_iter=5,debug_level=10,solvable="x",
110 # an alternative way to specify children is via the 'children' keyword.
111 # this is simply a matter of taste, since you may want to give the node's
112 # init-record first, as we do here.
113 # The value of children may be a list of children, a dict (see below),
114 # or a single child as here:
115 children = Meq.Condeq( # a condeq, with 2 children.
116 # What is this child going to be called? We see here a node def but
117 # no stub. This is an unnamed node. A name for it will be generated
118 # automatically, probably something of the form "parmN".
119 Meq.Parm([0,1]),
120 # What about here? Of course, a Meq.Add node, with two children
121 # of its own: a Meq.Constant with value 5, and a Parm named 'x'.
122 Meq.Add(5,ns.x << Meq.Parm(0,node_groups='Parm'))
123 ),
124 # stepchildren are specified via this keyword. A list or a single
125 # stepchild may be supplied.
126 stepchildren = [ Meq.Parm(0),Meq.Parm(1) ]
127 );
128
129 # As a final wrinkle on specifying children, note that some nodes
130 # may use child labels. For example, MeqUVW has children labelled
131 # 'radec_0' (phase center), 'xyz_0' (reference position), and 'xyz' (antenna
132 # position). MeqUVW has two children labelled 'radec_0' and 'radec'.
133 # If we defined a UVW node as:
134 # ns.uvw << Meq.UVW(ns.radec0,ns.xyz0,ns.xyz)
135 # we'd never be sure if the order of the children is right. This is where
136 # child labels help avoid mistakes. The first way to use labels is to
137 # pass in a dict of children:
138 phasecen = ns.phasecen << Meq.Constant([0.,0]);
139 srcpos = ns.srcpos << Meq.Constant([.1,.1]);
140 ns.lmn = Meq.LMN(children={'radec_0':phasecen,'radec':srcpos});
141 # This syntax is a bit cumbersome. Fortunately, you can also do it
142 # like this:
143 for st in stations:
144 ns.uvw(st) << Meq.UVW(radec=phasecen,xyz=ns.xyz(st),xyz_0=ns.xyz0(st));
145 # There's some potential for confusion here because both labelled children
146 # and initrec fields are specified via keyword arguments. How does TDL
147 # know the difference? The answer is, a keyword argument that is a node stub
148 # or a node def is treated as a child, and everything else is assumed to be
149 # an initrec field. Hence,
150 # ns.lmn << Meq.LMN(radec0=Meq.Constant([0,0]),radec=ns.position)
151 # ...correctly specifies two children, but
152 # ns.lmn << Meq.LMN(radec0=0,radec=ns.position)
153 # specifies an initrec field, initrec.radec0=0. So, you can use implicit
154 # constant nodes for regular children, but not for labelled ones.
155
156 # More examples of automatic node names.
157 # Now here's another interesting subtree:
158 ns.d3 << (ns.d4 << Meq.Parm(1)) * Meq.Parm + 5;
159 # This creates something like:
160 # 'd3' (MeqAdd)
161 # +-- 'multiply(d4,parm3)' (MeqMultiply)
162 # | +-- 'd4' (MeqParm)
163 # | +-- 'parm3' (MeqParm)
164 # +-- 'c5' q (MeqConstant)
165 # This illustrates two things:
166 # 1. Simple arithmetic (+ - * /) with nodes automatically expands into trees
167 # 2. Unnamed function nodes (such as the MeqMultiply here) are assigned
168 # names based on their child names.
169
170 # Note that (quite obviously if you think about it)
171 ns.d5 << 1 + 2
172 # does not define a subtree, but just one constant node. This is because
173 # Python interprets and computes '1+2' directly.
174
175 # Subscopes
176 # Subscopes are a useful way to modularize subtrees. Consider a function
177 # that creates a subtree.
178 def subtree (ns):
179 ns.d6 << ( (ns.d7 << Meq.Parm) + 5 );
180 # of course we can call it only once, subsequent calls will redefine
181 # the same node names
182 subtree(ns);
183 # how do we create the same subtree multiple times with different names?
184 # Using qualifiers is one idea:
185 def subtree1 (ns,*q,**kq):
186 ns.d6(*q,**kq) << ( (ns.d7(*q,**kq) << Meq.Parm) + 5 );
187 subtree1(ns,'a'); # d6:a d7:a
188 subtree1(ns,'b'); # d6:b d7:b
189 # this however is cumbersome, and may get in the way of using qualifiers
190 # for something else (e.g. stations, etc.). Subscopes come to the rescue:
191 ns1 = ns.Subscope('a'); # ns1 is also a node scope object
192 subtree(ns1); # creates 'a::d6', 'a::d7'
193 ns2 = ns.Subscope('b',1,x=2); # subscopes can have qualifiers
194 subtree(ns2); # creates 'b:1:x=2::d6', 'b:1:x=2::d7'
195
196 # More fun with automatic node naming. First, note that we can assign
197 # a node def directly to the 'ns' object to create an auto-named node
198 # without specifying a stub.
199 ns << Meq.Cos(ns.x(1)-ns.y(1));
200 # now, note that node qualifiers are retained: the above creates
201 # 'cos(sub(x,c1)):1', with a single '1' qualifier retained
202 # Compare this with
203 ns << Meq.Add(Meq.Sqr(ns.x(1)),Meq.Sqr(ns.y(1)),Meq.Sqr(ns.z(2)));
204 # this creates 'add(sqr(x),sqr(y),sqr(z)):1:2', so the rule is:
205 # identical qualifiers are merged, and differing qualifiers are accumulated
206
207 # note the treatment of keyword qualifers
208 ns.e(a=1) << 0;
209 ns.f(a=1) << 1;
210 ns.f(a=2) << 1;
211 ns.f(b=1) << 2;
212 ns << ns.e(a=1) + ns.f(a=1); # creates 'add(e,f):a=1'
213 ns << ns.e(a=1) + ns.f(a=2); # creates 'add(e,f):a=1,2'
214 ns << ns.e(a=1) + ns.f(b=1); # creates 'add(e,f):a=1:b=1'
215
216 # Finally, a word on orphans and node groups. Remember the orphans_are_roots
217 # setting above which we set to True. If we had set it to False, all
218 # the nodes defined above would have been deleted afterwards as
219 # "orphaned". A node (or a branch) is considered orphaned when the
220 # following two conditions are met:
221 # 1. It has no parents (i.e. it is a root node)
222 # 2. The node stub is not referred to from anywhere else in Python
223 # (i.e. it is not assigned to any variables or containers)
224 # Automatic orphan cleaning is handy if you want to create a bunch of
225 # subtrees in advance, and only leave behind the ones that are actually
226 # used. This means that you must explicitly hold refs to the true "root"
227 # nodes by, e.g., assigning them to a container that does not go out
228 # of scope when our define_forest() returns.
229
230 # A NodeGroup provides a convenient container for this. It is, in fact,
231 # nothing more than a dict that redefines operator << so that
232 # group << node
233 # is equivalent to
234 # group[node.name] = node;
235 # and also redefines the "in" operator so that saying 'node in group' is
236 # equivalent to 'node.name in group'.
237 # Every NodeScope object has a predefined "root" group available as
238 # ns.ROOT. So, all we need to do to mark nodes as true roots and not orphans
239 # is say:
240 ns.ROOT << ns.e(a=1);
241 # You can define a node and assigns it to roots in one statement, but
242 # take care with the parentheses (<< binds left to right)
243 ns.ROOT << ( ns << ns.e(a=1) + ns.f(b=1) ); # creates 'add(e,f):a=1:b=1'
244
245
246 def test_forest (mqs,parent):
247 """test_forest() is a standard TDL name. When a forest script is
248 loaded by, e.g., the browser, and the "test" option is set to true,
249 this method is automatically called after define_forest() to run a
250 test on the forest. The 'mqs' argument is a meqserver proxy object.
251 """;
252 from Timba.Meq import meq
253 # run tests on the forest
254 cells = meq.cells(meq.domain(0,1,0,1),num_freq=6,num_time=4);
255 request = meq.request(cells,eval_mode=0);
256 mqs.meq('Node.Execute',record(name='x',request=request));
257 mqs.meq('Save.Forest',record(file_name='tile_test.forest.save'));
258 # execute request on solver
259 request = meq.request(cells,eval_mode=1);
260 # mqs.meq('Node.Set.Breakpoint',record(name='solver'));
261 # mqs.meq('Debug.Set.Level',record(debug_level=100));
262 mqs.meq('Node.Execute',record(name='solver',request=request));
263
264 def tdl_job_test1 (mqs,parent):
265 """this is a test job. You can see it appear in the menu automatically."""
266 pass;
267
268 def tdl_job_test2 (mqs,parent):
269 """this is another test job. You can see it appear in the menu automatically."""
270 pass;
271
272
273 # this is the testing branch, executed when the script is run directly
274 # via 'python script.py'
275
276 if __name__ == '__main__':
277 # from Timba.Meq import meqds
278
279 Timba.TDL._dbg.set_verbose(5);
280
281 ns = NodeScope(test=True);
282
283 define_forest(ns);
284
285 # resolves nodes
286 ns.Resolve();
287
288 # test_forest(None);
289
290 # # feeds repository to back-end to create the forest
291 # MqsBackend.create_forest(ns.repository(),mqs);
292 #
293 # test_forest(meqds.mqs());
