11Operations
22==========
33
4- At a high level, an operation is a node in a computation graph.
5- Graphtik uses an :class: `.Operation ` class to abstractly represent these computations.
6- The class specifies the *requirements * for a function to participate
7- in a computation graph; those are its input-data **needs **, and the output-data
8- it **provides **.
4+ At a high level, an :term: `operation ` is a function in a computation :term: `pipeline `,
5+ abstractly represented by the :class: `.Operation ` class.
6+ This class specifies the :term: `dependencies <dependency> ` of the *operation *
7+ in the *pipeline *.
98
10- The :class: `.FunctionalOperation ` provides a lightweight wrapper
11- around an arbitrary function to define those specifications.
9+ The :class: `.FunctionalOperation ` provides a concrete lightweight wrapper
10+ around any arbitrary function to define those *dependencies *.
11+ Instead of constructing it directly, prefer to instantiate it by calling
12+ the :func: `.operation() ` factory.
1213
13- .. autoclass :: graphtik.op.FunctionalOperation
14- :members: __init__, __call__, compute
15- :noindex:
16-
17- The ``operation `` builder factory
18- ---------------------------------
19-
20- There is a better way to instantiate an ``FunctionalOperation `` than simply constructing it:
21- use the ``operation `` builder class:
22-
23- .. autoclass :: graphtik.operation
24- :members: withset, __call__
25- :special-members:
26- :noindex:
2714
2815Operations are just functions
2916------------------------------
3017
31- At the heart of each `` operation ` ` is just a function, any arbitrary function.
32- Indeed, you can instantiate an `` operation `` with a function and then call it
18+ At the heart of each `operation ` is just a function, any arbitrary function.
19+ Indeed, you can wrap an existing function into an ` operation `, and then call it
3320just like the original function, e.g.:
3421
3522 >>> from operator import add
3623 >>> from graphtik import operation
3724
38- >>> add_op = operation(name = ' add_op' , needs = [' a' , ' b' ], provides = [' a_plus_b' ])(add)
25+ >>> add_op = operation(add,
26+ ... needs= [' a' , ' b' ],
27+ ... provides= [' a_plus_b' ])
28+ >>> add_op
29+ FunctionalOperation(name='add', needs=['a', 'b'], provides=['a_plus_b'], fn='add')
3930
4031 >>> add_op(3 , 4 ) == add(3 , 4 )
4132 True
4233
34+ But ``__call__() `` is just to facilitate quick experimentation - it does not
35+ perform any checks or matching of *needs */*provides * to function arguments
36+ & results (which happen when :term: `pipeline `\s :term: `compute `).
37+
38+ The way Graphtik works is by invoking their :meth: `.Operation.compute() ` method,
39+ which, among others, allow to specify what results you desire to receive back
40+ (read more on :ref: `graph-computations `).
41+
4342
4443Specifying graph structure: ``provides `` and ``needs ``
4544------------------------------------------------------
4645
47- Of course, each ``operation `` is more than just a function.
48- It is a node in a computation graph, depending on other nodes in the graph for input data and
49- supplying output data that may be used by other nodes in the graph (or as a graph output).
50- This graph structure is specified via the ``provides `` and ``needs `` arguments
51- to the ``operation `` constructor. Specifically:
46+ Each :term: `operation ` is a node in a computation :term: `graph `,
47+ depending and supplying data from and to other nodes (via the :term: `solution `),
48+ in order to :term: `compute `.
5249
53- * ``provides ``: this argument names the outputs (i.e. the returned values) of a given ``operation ``.
54- If multiple outputs are specified by ``provides ``, then the return value of the function
55- comprising the ``operation `` must return an iterable.
50+ This graph structure is specified (mostly) via the ``provides `` and ``needs `` arguments
51+ to the :func: `.operation ` factory, specifically:
5652
57- * ``needs ``: this argument names data that is needed as input by a given `` operation ``.
58- Each piece of data named in needs may either be provided by another `` operation ` `
59- in the same graph (i.e. specified in the `` provides `` argument of that `` operation ``),
60- or it may be specified as a named input to a graph computation
61- (more on graph computations :ref: ` here < graph-computations > `).
53+ ``needs ``
54+ this argument names the list of (positionally ordered) :term: ` inputs ` data the ` operation `
55+ requires to receive from * solution *.
56+ The list corresponds, roughly, to the arguments of the underlying function
57+ (plus any :term: ` sideffects `).
6258
63- When many operations are composed into a computation graph (see :ref: `graph-composition ` for more on that),
64- Graphtik matches up the values in their ``needs `` and ``provides `` to form the edges of that graph.
59+ It can be a single string, in which case a 1-element iterable is assumed.
6560
66- Let's look again at the operations from the script in :ref: ` quick-start `,
67- for example:
61+ :seealso: :term: ` needs `, :term: ` modifier `, :attr: ` .FunctionalOperation.needs `,
62+ :attr: ` .FunctionalOperation.op_needs `, :attr: ` .FunctionalOperation._fn_needs `
6863
69- >>> from operator import mul, sub
70- >>> from functools import partial
71- >>> from graphtik import compose, operation
72-
73- >>> # Computes |a|^p.
74- >>> def abspow (a , p ):
75- ... c = abs (a) ** p
76- ... return c
64+ ``provides ``
65+ this argument names the list of (positionally ordered) :term: `outputs ` data
66+ the operation provides into the *solution *.
67+ The list corresponds, roughly, to the returned values of the `fn `
68+ (plus any :term: `sideffects ` & :term: `alias `\e s).
7769
78- >>> # Compose the mul, sub, and abspow operations into a computation graph.
79- >>> graphop = compose(" graphop" ,
80- ... operation(name = " mul1" , needs = [" a" , " b" ], provides = [" ab" ])(mul),
81- ... operation(name = " sub1" , needs = [" a" , " ab" ], provides = [" a_minus_ab" ])(sub),
82- ... operation(name = " abspow1" , needs = [" a_minus_ab" ], provides = [" abs_a_minus_ab_cubed" ])
83- ... (partial(abspow, p = 3 ))
84- ... )
70+ It can be a single string, in which case a 1-element iterable is assumed.
8571
86- .. Tip ::
87- Notice the use of :func: `functools.partial() ` to set parameter ``p `` to a constant value.
88-
89- The ``needs `` and ``provides `` arguments to the operations in this script define
90- a computation graph that looks like this (where the oval are *operations *,
91- squares/houses are *data *):
92-
93- .. graphtik ::
94-
95- .. Tip ::
96- See :ref: `plotting ` on how to make diagrams like this.
72+ If they are more than one, the underlying function must return an iterable
73+ with same number of elements (unless it :term: `returns dictionary `).
9774
75+ :seealso: :term: `provides `, :term: `modifier `, :attr: `.FunctionalOperation.provides `,
76+ :attr: `.FunctionalOperation.op_provides `, :attr: `.FunctionalOperation._fn_provides `
9877
9978.. _aliases :
10079
@@ -110,87 +89,113 @@ on the `provides` side:
11089 ... name= " `provides` with `aliases`" ,
11190 ... needs= " anything" ,
11291 ... provides= " real thing" ,
113- ... aliases= (" real thing" , " phony" ))()
92+ ... aliases= (" real thing" , " phony" ))
11493
11594.. graphtik ::
11695
117- Instantiating operations
118- ------------------------
11996
120- There are several ways to instantiate an ``operation ``, each of which might be more suitable for different scenarios.
97+ Considerations for when building pipelines
98+ ------------------------------------------
99+ When many operations are composed into a computation graph, Graphtik matches up
100+ the values in their *needs * and *provides * to form the edges of that graph
101+ (see :ref: `graph-composition ` for more on that), like the operations from the script
102+ in :ref: `quick-start `:
121103
122- Decorator specification
123- ^^^^^^^^^^^^^^^^^^^^^^^
104+ >>> from operator import mul, sub
105+ >>> from functools import partial
106+ >>> from graphtik import compose, operation
124107
125- If you are defining your computation graph and the functions that comprise it all in the same script,
126- the decorator specification of `` operation `` instances might be particularly useful,
127- as it allows you to assign computation graph structure to functions as they are defined.
128- Here's an example:
108+ >>> def abspow ( a , p ):
109+ ... """ Compute |a|^p. """
110+ ... c = abs (a) ** p
111+ ... return c
129112
130- >>> from graphtik import operation, compose
113+ >>> # Compose the mul, sub, and abspow operations into a computation graph.
114+ >>> graphop = compose(" graphop" ,
115+ ... operation(mul, needs = [" a" , " b" ], provides = [" ab" ]),
116+ ... operation(sub, needs = [" a" , " ab" ], provides = [" a_minus_ab" ]),
117+ ... operation(name = " abspow1" , needs = [" a_minus_ab" ], provides = [" abs_a_minus_ab_cubed" ])
118+ ... (partial(abspow, p = 3 ))
119+ ... )
120+ >>> graphop
121+ NetworkOperation('graphop',
122+ needs=['a', 'b', 'ab', 'a_minus_ab'],
123+ provides=['ab', 'a_minus_ab', 'abs_a_minus_ab_cubed'],
124+ x3 ops: mul, sub, abspow1)
131125
132- >>> @ operation(name = ' foo_op' , needs = [' a' , ' b' , ' c' ], provides = ' foo' )
133- ... def foo (a , b , c ):
134- ... return c * (a + b)
135126
136- >>> graphop = compose(' foo_graph' , foo)
127+ - Notice that if ``name `` is not given, it is deduced from the function name.
128+ - Notice the use of :func: `functools.partial() ` to set parameter ``p `` to a constant value.
129+ - And this is done by calling once more the returned "decorator* from :func: `operation() `,
130+ when called without a functions.
131+
132+ The ``needs `` and ``provides `` arguments to the operations in this script define
133+ a computation graph that looks like this:
137134
138135.. graphtik ::
136+ .. Tip ::
137+ See :ref: `plotting ` on how to make diagrams like this.
139138
139+ Builder pattern
140+ ^^^^^^^^^^^^^^^
141+ There 2 ways to instantiate an :class: `.FunctionalOperation `\s , each one suitable
142+ for different scenarios, and so far we have only seen the 1st one:
140143
141- Functional specification
142- ^^^^^^^^^^^^^^^^^^^^^^^^
144+ We've seen that calling manually :func: ` .operation() ` allows putting into a pipeline
145+ functions that are defined elsewhere (e.g. in another module, or are system functions).
143146
144- If the functions underlying your computation graph operations are defined elsewhere
145- than the script in which your graph itself is defined (e.g. they are defined in another module,
146- or they are system functions), you can use the functional specification of ``operation `` instances:
147+ But that method is also useful if you want to create multiple operation instances
148+ with similar attributes, e.g. ``needs ``:
147149
148- >>> from operator import add, mul
149- >>> from graphtik import operation, compose
150+ >>> op_factory = operation(needs = [' a' ])
150151
151- >>> add_op = operation( name = ' add_op ' , needs = [ ' a ' , ' b ' ], provides = ' sum ' )(add)
152- >>> mul_op = operation( name = ' mul_op ' , needs = [ ' c ' , ' sum ' ], provides = ' product ' )(mul)
152+ Notice that we specified a ` fn `, in order to get back a :class: ` .FunctionalOperation `
153+ instance (and not a decorator).
153154
154- >>> graphop = compose( ' add_mul_graph ' , add_op, mul_op)
155+ >>> from functools import partial
155156
156- .. graphtik ::
157+ >>> def mypow (a , p = 2 ):
158+ ... return a ** p
157159
160+ >>> pow_op2 = op_factory.withset(fn = mypow, provides = " ^2" )
161+ >>> pow_op3 = op_factory.withset(fn = partial(mypow, p = 3 ), name = ' pow_3' , provides = ' ^3' )
162+ >>> pow_op0 = op_factory.withset(fn = lambda a : 1 , name = ' pow_0' , provides = ' ^0' )
158163
159- The functional specification is also useful if you want to create multiple ``operation ``
160- instances from the same function, perhaps with different parameter values, e.g.:
164+ >>> graphop = compose(' powers' , pow_op2, pow_op3, pow_op0)
165+ >>> graphop
166+ NetworkOperation('powers', needs=['a'], provides=['^2', '^3', '^0'], x3 ops:
167+ mypow, pow_3, pow_0)
161168
162- >>> from functools import partial
163169
164- >>> def mypow ( a , p = 2 ):
165- ... return a ** p
170+ >>> graphop( a = 2 )
171+ {'a': 2, '^2': 4, '^3': 8, '^0': 1}
166172
167- >>> pow_op1 = operation(name = ' pow_op1' , needs = [' a' ], provides = ' a_squared' )(mypow)
168- >>> pow_op2 = operation(name = ' pow_op2' , needs = [' a' ], provides = ' a_cubed' )(partial(mypow, p = 3 ))
173+ .. graphtik ::
169174
170- >>> graphop = compose(' two_pows_graph' , pow_op1, pow_op2)
171175
172- A slightly different approach can be used here to accomplish the same effect
173- by creating an operation "builder pattern":
176+ Decorator specification
177+ ^^^^^^^^^^^^^^^^^^^^^^^
174178
175- >>> def mypow (a , p = 2 ):
176- ... return a ** p
179+ If you are defining your computation graph and the functions that comprise it all in the same script,
180+ the decorator specification of ``operation `` instances might be particularly useful,
181+ as it allows you to assign computation graph structure to functions as they are defined.
182+ Here's an example:
177183
178- >>> pow_op_factory = operation(mypow, needs = [ ' a ' ], provides = ' a_squared ' )
184+ >>> from graphtik import operation, compose
179185
180- >>> pow_op1 = pow_op_factory( name = ' pow_op1 ' )
181- >>> pow_op2 = pow_op_factory.withset( name = ' pow_op2 ' , provides = ' a_cubed ' )(partial(mypow, p = 3 ))
182- >>> pow_op3 = pow_op_factory( lambda a : 1 , name = ' pow_op3 ' )
186+ >>> @ operation( name = ' foo_op ' , needs = [ ' a ' , ' b ' , ' c ' ], provides = ' foo ' )
187+ ... def foo ( a , b , c ):
188+ ... return c * (a + b )
183189
184- >>> graphop = compose(' two_pows_graph ' , pow_op1, pow_op2, pow_op3 )
185- >>> graphop( a = 2 )
186- {'a': 2, 'a_squared': 4, 'a_cubed': 1}
190+ >>> graphop = compose(' foo_graph ' , foo )
191+
192+ .. graphtik ::
187193
188- .. Note ::
189- You cannot call again the factory to overwrite the *function *,
190- you have to use either the ``fn= `` keyword with ``withset() `` method or
191- call once more.
192194
193195
194196Modifiers on `operation ` `needs ` and `provides `
195197-----------------------------------------------
196- see `mod: `.modifiers`.
198+ Annotations on a `dependency ` such as `optionals ` & `sideffects ` modify their behavior,
199+ and eventually the :term: `pipeline `.
200+
201+ Read `mod: `.modifiers` for more.
0 commit comments