<jupyter_start><jupyter_text>Elements of a FunctionThe basic elements by which the code inside a function is interpreted by the Python shell are in many ways identical to any other block of code. As previously discussed, the contained code is interpreted according to the mechanisms of control flow to produce and manipulate objects in memory. While the syntax and flow of execution is very similar, the context of the execution of this code differs in two important ways:* a function may be passed information from its calling context via ***arguments***. * a function may return information to that context via ***return values***.Without these elements, functions serve simply as a form of control flow, directing the interpretation of our scripts to distant files or blocks of code. Geometric subdivision will be our guide through a series of progressively elaborate functions. Each will operate on collections of linear elements, and will manipulate a common data structure that is referred to as a “face”, defined as a collection of three or four connected Segments that describe triangles or quadrilaterals. Both data structure and vocabulary anticipates the Decod.es Mesh.    ProceduresA ***procedure*** is the simplest possible regular function, it is a Python function that accepts no arguments and returns no values.Each time the following procedure `subdivide()` is called, the most recent set of four-sided faces is subdivided, the result of which is appended to the end of a collection of “generations” of faces, `fgen`s. By procedure end, the entire history of subdivision has been stored with a record for each step in the process.<jupyter_code>"""
Edge-to-Center Rectangular Subdivision
Given a division value div_val, and a data structure for faces 
(defined  by a properly structured three-dimensional collection of 
Segments),  a recursive subdivision routine is performed to a given 
number of generations.
"""
def subdivide():
    faces, subfaces = fgens[-1], []
    for fac in faces:
        #{b} a Point at the center of the face
        cen = Point.centroid([seg.ept for seg in fac])
        # midpoints of each edge
        pts = [seg.eval(div_val) for seg in fac]
        # starting edge sub-Segments
        ssubs = [Segment(seg.spt,pt) for seg,pt in zip(fac,pts)]
        # ending edge sub-Segments
        esubs = [Segment(pt,seg.ept) for seg,pt in zip(fac,pts)]
        # middle sub-Segments
        msubs = [Segment(pt,cen) for pt in pts]
        
        # weave together subdivided segs into four new faces
        subfaces.append( (ssubs[0],msubs[0],msubs[3].inverted(),esubs[3]) )
        subfaces.append( (ssubs[1],msubs[1],msubs[0].inverted(),esubs[0]) )
        subfaces.append( (ssubs[2],msubs[2],msubs[1].inverted(),esubs[1]) )
        subfaces.append( (ssubs[3],msubs[3],msubs[2].inverted(),esubs[2]) )
        
    fgens.append(subfaces)
    
# {a} subdivide for a given number of generations
for n in range(gens): 
    subdivide()<jupyter_output><empty_output><jupyter_text>The geometric details of the procedure are relatively simple. * First, some intermediate geometric variables are defined. Each of the four given Segments are divided into two at a Point determined by `div_val`, and stored in the variables `ssubs` and `esubs`. A third set of Segments then join the centroid of the face with the division Point, and are stored in the variable `msubs`.* Next, these three sets of Segments are woven together to form four new sub-faces, which are appended to the temporary collection `subfaces`, with care taken to ensure that the winding direction of each Segment of the new faces is in alignment by making use of the `inverted()` method. * Finally, the newly generated subfaces are stored such that they may themselves be subdivided in subsequent steps.   Notice that the `fgens` data structure persists between subdivisions, and is referenced within the `subdivide()` procedure, but is not passed in as an argument. To understand the ramifications of this arrangement, let’s take a look at the object model diagram as it would appear while the procedure is executing.We can see that the faces variable is defined within the scope of the `subdivide()` procedure, but refers to the first item in the `fgens` collection - an object that exists outside the scope of this procedure. Referring to objects in a global scope beyond the one currently executing is a legal operation in Python, it is ***inadvisable***.   Recovering Values from FunctionsThe syntactical template for returning values consists of the keyword `return`, followed by an expression that is evaluated to a single value or a collection of values. When the function is executed, the object that results from the first evaluation of a return statement is substituted for the function call in any calling statements.    def function_name():        do_some_things        return some_thingThe `return` statement signals the termination of a block of code. Even though a single function might contain several return statements, only one of them will be evaluated for any given function call. After an object is returned, the function will exit back to the calling context, and ***no other statements will be executed***.Consider the following common pattern of code, in which either an_object or another_object is returned.    def function_name():        if a_boolean:            return an_object        return another_object        raise OMGWeAreDoomedException A revision of the subdivision script presents a chance to see the return statement in action. This revised function accomplishes a similar task to the previous version without the contortions to assign values to a collection outside of the scope of the function, instead creating and returning a new collection of faces each time the function is called.<jupyter_code>"""
Corner-to-Center Triangular Subdivision
Each face in a given collection of faces is iteratively removed from 
the collection and subdivided, the results of which are collected in 
the subfaces List. At the end of each cycle, the subfaces List 
overwrites the faces List, and the process continues.
"""
def subdivide():
    subfaces = []
    # so long as faces remain,
    while faces:
        # remove the first remaining face in faces
        fac = faces.pop()
        cen = Point.centroid([seg.ept for seg in fac])
        # sub-Segments from each corner to the center
        csubs = [Segment(seg.ept,cen) for seg in fac]
        
        # {b} weave together subdivided Segments into three new faces
        subfaces.append( (fac[0],csubs[0],csubs[2].inverted())  )
        subfaces.append( (fac[1],csubs[1],csubs[0].inverted())  )
        subfaces.append( (fac[2],csubs[2],csubs[1].inverted())  )
        
    return subfaces

for n in range(gens): 
    # {a} overwrite faces with the results of the subdivision
    faces = subdivide()<jupyter_output><empty_output><jupyter_text>While the variable `faces` is still referenced from outside the scope of the procedure, no objects are appended to it. Instead, objects are iteratively removed from faces using the `pop()` method, and a new collection subfaces is defined inside the function in order to store the subdivided faces.  At the moment described by the nearby object model diagram, the face `fac` has already been removed from the out-of-scope faces variable, and the first of three subfaces is under construction and is being appended to the subfaces collection. Following the completion of the function, this collection is returned to the calling context, where it overwrites the original collection `faces`.  This next object model diagram shows that the return value of the function will effectively replace the current contents of `faces`. The cycle may then continue to any given number of generations, without the need for the multi-generational collection of faces representing the entire history of subdivision used before. Even though this subdivision constructs three-sided instead of four-sided faces, the routine operates geometrically much like the previous version, constructing the intermediate geometric variables cen and csubs, before weaving together properly aligned sub-faces.An alternative construction is possible, that connects the middle of each edge of a given face to produce four three-sided sub-faces. This requires a more involved set of intermediate geometries.<jupyter_code>"""
Edge-to-Edge Triangular Subdivision
"""
def subdivide():
    subfaces = []
    while faces:
        fac = faces.pop()
        # midpoints of each edge
        pts = [seg.eval(0.5) for seg in fac]
        ssubs = [Segment(seg.spt,pt) for seg,pt in zip(fac,pts)]
        esubs = [Segment(pt,seg.ept) for seg,pt in zip(fac,pts)]
        msubs = [Segment(pa,pb) for pa,pb in zip(pts,pts[-1:]+pts[:-1])]
        
        subfaces.append( tuple([sub.inverted() for sub in msubs]) )
        subfaces.append( (ssubs[0],msubs[0],esubs[2]) )
        subfaces.append( (ssubs[1],msubs[1],esubs[0]) )
        subfaces.append( (ssubs[2],msubs[2],esubs[1]) )
        
    return subfaces

for n in range(gens): faces = subdivide()<jupyter_output><empty_output><jupyter_text>Passing Values to FunctionsThe primary instrument by which a function receives information from its calling context is via arguments. In this section we build upon our basic working understanding of arguments, and detail a number of more refined approaches for structuring them.  Passing Values via KeywordTypically, values are related to function arguments by their position in the calling statement. However, they may also be related by specifying keyword relationships.<jupyter_code>"""
Passing Values via Keyword
Functions may be called using a positional assignment of arguments, 
or by keyword. The last two lines of this code are equivalent calls 
to the cs_eval_cyl function.
"""    
def cs_eval_cyl(cs,rad,ang):
    return cs.eval( Point( rad*math.cos(ang), rad*math.sin(ang) ) )

# positional argument assignment
cs_eval_cyl( CS(), 1.0, math.pi )
# keyword argument assignment
cs_eval_cyl( CS(), ang = math.pi, rad = 1.0)<jupyter_output><empty_output><jupyter_text>Default ValuesPython offers the ability for function arguments to specify default values.    def function_name( arg_one, arg_two = expression ):        do_some_things        return some_thingThere is no restriction on the number of arguments with default values for a function, but these must always conclude the sequence of arguments.<jupyter_code>"""
Vector Interpolation
Calculates and returns the vector that results from an interpolation 
between two given vectors at parameter t.
"""
@staticmethod
def interpolate( va, vb, t=0.5 ):
    x = (1-t) * va.x + t * vb.x
    y = (1-t) * va.y + t * vb.y
    z = (1-t) * va.z + t * vb.z
    return Vec(x,y,z)<jupyter_output><empty_output><jupyter_text>The default value expression may be any expression at all, including those that require short calculations such as `t=a+10`, so long as the expression reduces to a value. Default values might include both primitive and structured objects, but the latter must be handled with care. If referring to any objects out of scope, itis recommended that the primitive `None` type be used as the default value, and then the intended out-of-scope value assigned in the body of the function instead.<jupyter_code>"""
CS on XY Plane
Returns a coordinate system on the world xy plane at a specified 
coordinate location. Optionally, one may define a vector that 
controls the orientation of the x_axis of the resulting CS on the 
Plane. The z coordinate of this vector is ignored. If this vector 
is not given, the world x-vector is used.
"""
@staticmethod
def on_xy( x, y, vec=None ):
    # default orientation vector is the unit-x vector
    if vec is None: vec = UX
    # ignore the z-coordinate of the given Vec
    vec.z = 0
    return CS( Point(x,y,0), vec, vec.cross(UZ.inverted()) )
    
# a CS at (1,1) at a 45deg angle
cs = CS.on_xy(1,1,vec=Vec(1,1))<jupyter_output><empty_output><jupyter_text>Packing and Unpacking ArgumentsEach of the techniques presented in this section support the defining or calling of a regular function through the relating of a sequence of arguments with some form of collection - typically a List or a Dict.* ***Packing*** refers to transferring a sequence of given arguments into a collection.* ***Unpacking*** refers to transferring the contents of a collection with a sequence of arguments.* ***Positional*** packing or unpacking forms relationships by the order in which they are found in a sequence.* ***Keyword*** packing or unpacking does so by keys in a Dict.  Positional Argument PackingThis technique allows functions to be defined such that they receive an arbitrary number of arguments. The required format calls for a single argument proceeded by an asterisk (`*`).     def function_name( *args ):        do_some_thingsWhen a function defined in this way is called, any arguments provided are packed in to a single Tuple that resides within the scope of the function, and may be referred to in this context using the argument name provided. By convention, such an argument is given the name `*args`.<jupyter_code>"""
Threading Points with Positional Argument Packing
A function may be configured to receive any number of arguments using 
packing. Here, any number of given Points are threaded together to 
form a closed loop of Segments. Similar functionality could be 
achieved by defining a function that takes a single List of Points 
as an argument.
"""
def thread_pts(*pts):
    segs = [ Segment(pa,pb) for pa,pb in zip(pts[:-1],pts[1:]) ]
    return segs + [Segment(pts[-1],pts[0])]

segs = thread_pts(pa,pb,pc,pd,pe)<jupyter_output><empty_output><jupyter_text>Positional Argument UnpackingThe inverse of the above technique affords a similar compactness to the manner in which functions are called. In positional argument unpacking, the objects in a given sequence are unpacked and positionally related to the required arguments of a regular function. Again, the required format calls for a single value that takes the place of a sequence of arguments, preceded by an asterisk. When used in this manner, some refer to the (`*`) as the ***splat operator***:    function_name( *sequence )    This is useful for calling functions for which we already have arguments defined and stored in a collection.<jupyter_code>"""
Plotting Points with Positional Argument Unpacking
Given values already stored in a collection, we may provide a regular 
function with a series of positional arguments by using the asterisk 
operator. Used in this way, this operator is sometimes called the 
"splat" operator. 
"""
crd = (0,0)
pt = Point(*crd)

crds = [(1,1),(2,2),(3,3,1)]
pts = [Point(*crd) for crd in crds]<jupyter_output><empty_output><jupyter_text>Combining Positional Argument Packing and UnpackingThe `sort_by_angle()` function is a crafty one. By employing a combination of argument packing, regular arguments, and when called using positional argument unpacking, this function is able to receive collections of Points of any length, slice off the first Point and sort the rest, with much of the work done simply by virtue of the structure of its arguments.<jupyter_code>"""
Sorting Points by Polar Angle from First
Applying both argument packing and variable unpacking, all but the 
first of a given List of Points is sorted by its polar angle relative 
to the first Point in the List. A tuple of the first Point and a 
sorted List of the remaining Points is returned.
"""
def sort_by_angle(origin,*args):
    spts = sorted(args,key=lambda pt: Vec.ux().angle(Vec(origin,pt)))
    return origin, spts

opt, pts = sort_by_angle(*pts)<jupyter_output><empty_output><jupyter_text>Keyword Argument PackingThe key-value pairs found in a Dict play a role analogous to that of the sequences in the examples above. Defining a function that employs keyword argument packing follows a similar template, distinguished by a double-asterisk (`**`) proceeding an argument which is named, by convention, `kargs` or `kw`:    def function_name( **kargs ):        do_some_thingsWhen a function defined in this way is called, any arguments provided via keyword are packed into a single Dict. This technique holds significant utility in defining functions that can semantically be called in many different ways, thereby facilitating a variety of different means to achieve the same end.<jupyter_code>"""
CS on XY Plane using Keyword Argument Packing
Returns a coordinate system on the world xy plane at a specified 
coordinate location. One may define a vector that controls the 
orientation of the x_axis, or define the rotation from the world 
x-axis of the resulting CS on the Plane.
"""

def on_xy(x,y,**kargs):
    # define the orientation vector by whatever means
    if 'vec' in kargs: vec = kargs['vec']
    elif 'rot' in kargs: vec = Vec(math.cos( kargs['rot']),math.sin( kargs['rot'])) 
    else: vec = Vec.ux()
    
    # ignore the z-coordinate of the given Vec
    vec.z = 0
    return CS( Point(x,y,0), vec, vec.cross(UZ.inverted()) )
    
# a CS at (1,1) at a 60deg angle
cs = on_xy(1,1,rot=math.pi/3)
# a CS at (1,2) at a 45 deg angle
cs = on_xy(1,2,vec=Vec(1,1))<jupyter_output><empty_output><jupyter_text>Keyword Argument UnpackingHere, a Dict is provided as an argument by a calling statement. The objects found therein are automatically ‘unpacked’ and related to the required keyword arguments of the function by matching the key Strings of the Dict with the function keywords. Again, this construction is distinguished by a double-asterisk (`**`) proceeding the argument in a function call.    function_name( **dict )    Note that an error will be raised if the provided Dict is not structured properly, or does not contain the required keys.<jupyter_code>"""
Cylindrical Points using Keyword Argument Unpacking
Produces Points by cylindrical coordinates. A "table" of values is 
constructed as a collection of Dicts, each containing the required 
arguments for calling the cs_eval_cyl function. These are then 
iteratively passed to the function using keyword argument unpacking.
"""
def cs_eval_cyl(cs,rad,ang):
    return cs.eval( Point( rad*math.cos(ang), rad*math.sin(ang) ) )

pt_table = [
    { 'cs':CS(), 'rad':1.0, 'ang':0.0 },
    { 'cs':CS(), 'rad':2.0, 'ang':math.pi/2 },
    { 'cs':CS(), 'rad':3.0, 'ang':math.pi }
]

css = [cs_eval_cyl(**pt_dict) for pt_dict in pt_table]<jupyter_output><empty_output><jupyter_text>Flexible ArgumentationSince functions effectively package together relatively low-level collections of instructions for more general use, as an author of a function, we must consider the implications of this more general use. Here we consider an approach to authoring arguments as to maximize the potential utility of the function for others, by allowing for a flexible configuration of arguments. What is termed ***method overloading*** by other programming languages is achieved somewhat differently in Python.There are a number of ways to construct a Vec. While we previously just presented two, there are actually three in total:<jupyter_code>"""
Vec Three Ways
The Decod.es library offers three ways to create a Vec: va is 
constructed by three coordinates, vb is constructed as the vector 
that spans between two given Points, and vc is constructed by 
copying the members of a given Vec or Point.
"""            
va = Vec(0,0,0)
vb = Vec(pa,pb)
vc = Vec(pc)<jupyter_output><empty_output><jupyter_text>The same initialization function has been called with different configuration of arguments, and with different types being passed, producing a valid Vec in each case.<jupyter_code>"""
Vector Construction
To facilitate a flexible initialization of vectors, the Vec 
constructor method is designed to be able to receive three 
configuration of arguments. By assigning default values of None to 
each, and subsequently testing for the kind of argument that has 
been passed using a lambda function, we may reliably distinguish 
between these three configurations
"""
class Vec(Geometry):
    
    def __init__(self, a=None, b=None, c=None):
        # lambda to determine pointishness
        is_pt = lambda o: hasattr(o,'x') and hasattr(o,'y') and hasattr(o,'z')
        
        # we've been given three numbers, define a new Vec
        if a is not None and b is not None and c is not None :
            self.x, self.y, self.z = a ,b, c
        # we've been given two point-like things, create a Vec between
        elif is_pt(a) and is_pt(b):
            self.x, self.y, self.z = b.x - a.x, b.y - a.y, b.z - a.z
        # we've been given one point-like thing, copy it.
        elif is_pt(a):
            self.x, self.y, self.z = a.x, a.y, a.z<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.06988264717198922, 'fraction_numerical': 0.0049057329742208545, 'mean_word_length': 4.244136191677176, 'pattern_counts': {'":': 0, '<': 53, '<?xml version=': 0, '>': 53, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '462647', 'license': 'MIT', 'n_tokens_mistral': 5597, 'n_tokens_neox': 5387, 'n_words': 2778, 'path': '105 - Functions/175 - Elements of a Function.ipynb', 'repo_name': 'ksteinfe/decodes_ipynb', 'size': '20792'}