CLI commands (CLIconsole frontend)
Threadle comes with a command-line interface (CLI) application - Threadle.CLIconsole - that runs in the terminal or console on Windows, Linux and MacOS machines. Constituting a frontend to Threadle.Core, the CLIconsole has its own scripting language for working with network datasets. These Threadle-specific commands are thus used for creating, loading and saving networks and nodesets, importing data, specifying relational layers and nodal attributes, and adding, removing and querying about nodes, edges, affiliations and node attributes.
CLIconsole commands
Notations used in the list of commands:
- [var:type] : Refers to the name of a stored variable of specified type.
- *arg : Indicates an optional argument. Must be named explicitly.
- *arg1|*arg2 : Indicates two optional argument, where only one will be used.
- [uint], [str], [double] : Placeholder for data types (unsigned integer, string, double-size floating point).
- ['option1'(default), 'option2'] : Indicates possible values for an argument. If an argument is optional, the default value is indicated.
- [var:structure] = command(...) : Indicates that the command will create a structure that has to be assigned to a variable.
- [str] = command(...) : Indicates that the command will output a value (string in this case) on the command line.
- exit
- Exits the CLI console application, releasing all resources and stored variables.
- addaff(network = [var:network], layername = [str], nodeid = [uint], hypername = [str], *addmissingnode = ['true'(default),'false'], *addmissingaffiliation = ['true'(default),'false'])
- Adds an affiliation between node 'nodeid' and the hyperedge named 'hypername' in layer 'layername' in network [var:network]. The specified layer must be 2-mode. If the node-hyperedge affiliation already exists, nothing happens. If the specified node id does not exist in the Nodeset, it is by default created and added, but by setting 'addmissingnodes' to 'false' prevents this. If the specified hyperedge does not exist, it is by default created and added, but by setting 'addmissingaffiliations' to 'false' prevents this.
- addedge(network = [var:network], layername = [str], node1id = [uint], node2id = [uint], *value = [float(default:1)], *addmissingnodes = ['true'(default), 'false'])
- Creates and adds an edge between node1id and node2id in the specified layer 'layername' (which must be 1-mode) in network 'network'. If the layer is directional, node1id is the source (from) and node2id is the destination (to). The edge value 'value' is optional, defaulting to 1. If the specified node id's do not exist in the Nodeset, they are by default created and added, but by setting 'addmissingnodes' to 'false' prevents this.
- addhyper(network = [var:network], layername = [str], hypername = [str], *nodes = [semicolon-separated uints], *addmissingnodes = ['true'(default),'false'])
- Creates and adds a hyperedge (aka context or affiliation) called 'hypername' to layer 'layername' in the specified network. The layer must be 2-mode. This can then represent a particular school class or specific social event to which a set of nodes are affiliated. If there already is a hyperedge with the specified name, that is first removed. Can be provided with an optional list of nodes that are connected by this hyperedge. If the specified node id's do not exist in the Nodeset, they are by default created and added, but by setting 'addmissingnodes' to 'false' prevents this. Duplicate node id values are discarded.
- addlayer(network = [var:network], layername = [str], mode = ['1','2'], *directed = ['true','false'(default)], *valuetype = ['binary'(default),'valued'], *selfties=['true','false'(default)])
- Adds a relational layer for a network, which can be either 1-mode or 2-mode (with hyperedges). For 1-mode layers, there are additional settings for the type of ties that exist, specifying edge directionality, edge value type and whether selfties are allowed or not. For 2-mode layer, these settings do not matter: all ties are deemed binary. A network must have at least one layer defined, referred to when adding edges between nodes and nodes-affiliations as well as when importing edges from file to a layer.
- addnode(structure = [var:structure], nodeid = [uint])
- Creates and adds a node with id [nodeid] and adds it to the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure]. Note that the node id is what makes each node unique, and it must be an unsigned integer.
- [bool] = checkedge(network = [var:network], layername = [str], node1id = [int], node2id = [int])
- Checks if an edge exists between node1id (from) and node2id (to) in the specified layer 'layername', which can be either 1-mode or 2-mode. If the layer is 1-mode directional, node1id is the source and node2id is the destination. Returns the string 'true' if an edge is found, otherwise 'false'. For 2-mode layers, an edge exists if the two nodes share at least one affiliation (hyperedge).
- clearlayer(network = [var:network], layername = [str])
- Removes all edges in the specified layer for the specified network (but keeps the layer). For 2-mode layers, this therefore removes all hyperedges.
- components(network = [var:network], layername = [str], *attrname = [str])
- Calculates the number of components for the specified layer and network, and storing the result as an integer node attribute representing which component it is part of. The node attribute is automatically named to the layername and 'components', but this can be overridden with the attrName parameter. To explore the number of components, use the getattrsummary() on this node attribute: the max value represents the number of components minus one. To calculate the size of a component: use the filter() command to create new nodesets for each value of this attribute and then use getnbrnodes() to get the size of the filtered nodesets. Not the smoothest right now, but it works!
- [var:network] = createnetwork(nodeset = [var:nodeset], *name = [str])
- Creates a network using the provided [var:nodeset], giving the network the optional name 'name' and assigning it to the variable [var:network].
- [var:nodeset] = createnodeset(*name = [str], *createnodes = [int(default:0)])
- Creates an empty nodeset and stores it in the variable [var:nodeset]. An optional internal name 'name' can be provided. The Nodeset is by default empty, but nodes can also be created by specifying the number of nodes with the optional 'createnodes' integer value. The created nodes will then have id values starting from 0.
- defineattr(structure = [var:structure], attrname = [str], attrtype = ['int','char','float','bool'])
- Defines a Node attribute for the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure]. The name of the node attribute is 'attrname' and its data type is one of 'int' (integer), 'char' (single character), 'float' (floating point), or 'bool' (boolean, true or false).
- degree(network = [var:network], layername = [str], *attrname = [str], *direction = ['in'(default),'out','both'])
- Calculates the degree centrality for the specified layer and network, and storing the result as a node attribute. The optional direction parameter decides whether the inbound (default) or the outbound ties should be counted, or - for layers with directional relations - if both in- and outbound ties should be counted. The node attribute is automatically named to the layername and the direction, but this can be overridden with the attrName parameter. For 2-mode layers, the direction argument is moot.
- delete(structure = [var:structure])
- Deletes (removes) the structure with the variable name [var:structure]. A Nodeset can not be deleted if it is currently used by another structure: first delete those structures. If deleting a network, the nodeset that it uses will remain.
- deleteall()
- Deletes (removes) all current structures stored in the variable space. Use with caution!
- [double] = density(network = [var:network], layername = [str])
- Calculates and returns the density of the layer 'layername' of the specified network. Treats all existing ties as binary ties. Works for both 1-mode and 2-mode networks: the 2-mode version uses a routine that might take a bit longer.
- dichotomize(network = [var:network], layername = [str], *cond = ['eq','ne','gt','lt','ge'(default),'le','isnull'(invalid),'notnull'(invalid)], *threshold = [float(default:1)], *truevalue = [float(default:1),'keep'], *falsevalue = [float(default:0),'keep'], *newlayername = [str])
- Takes a valued 1-mode layer (which it is only applicable for) and dichotomizes it, creating and storing this as a new layer. By default, all values equal to or greater to 1 will be converted to a binary tie and all other values represent a missing tie. Both the condition, the threshold value and the values resulting from a true vs. a false evaluation can be modified. If truevalue and falsevalue are kept at their default values, i.e. either containing 1 or 0, the resulting layer will be a binary network, otherwise the resulting layer will be valued. The directionality of the new layer will remain the same as the original layer. The new layer will be named as the provided layer, with the addition '-dichotomized', but the name of the new layer can also be specified with the 'newlayername' argument.
- dir(*path = [str])
- Gets the content of the current directory, or the directory specified by the optional 'dir' argument.
- [var:nodeset] = filter(nodeset = [var:nodeset2], attrname = [str], cond=['eq','ne','gt','lt','ge','le','isnull','notnull'], +attrvalue = [str])
- Creates and stores a new nodeset [var:nodeset] containing all the nodes in [var:nodeset2] that fulfills the specified condition 'cond' concerning the specified attribute 'attrname' and the reference value 'attrvalue'. The new nodeset and its nodes and attributes constitute a partial deep copy of the inbound nodeset, making them completely independent from each other. Note: when checking for 'isnull' or 'notnull', the 'attrvalue' argument can be ignored.
- generate(network = [var:network], layername = [str], type = ['er','ws','ba','2mode'], +p = [double], +k = [int], +beta = [double], +m = [int], +h = [int], +a = [int])
- Creates a random network of the specified type in the specified layer of the specified network. The layer must already exist and be binary: any would-be ties that exist in that layer will first be removed. Erdös-Renyi (type to 'er'): layer can be either directional or symmetric, with or without selfties; set p to the edge probability (which is also the overall network density). For Watts-Strogatz (type to 'ws'): layer must be symmetric without selfties; set k to mean degree (must be even), and beta to rewiring probability (0-1 range). For Barabasi-Albert (type to 'ba'): layer must be symmetric without selfties; set m to the degree/attachment parameter (i.e. edges per new node). Note that the arguments marked with (+) are compulsory arguments for each type and must be named; arguments for types not used can be ignored. Also possible to generate random 2-mode data: layer must then be 2-mode; set h to the number of hyperedges that should be created, and a to the average number of affiliations each node should have (drawing from the poisson distribution).
- generateattr(structure = [var:structure], attrname = [str], *attrtype = ['int'(default),'float','bool','char'], *min = [int(default:0)|float(default:0.0)], *max = [int(default:100)|float(default:1.0)], +p = [double(default:0.5)], +chars = [str](default:"m;f;o")
- Creates a new node attribute with the given name and type for the specific nodeset, and sets a random attribute value to each node in this nodeset according to the provided parameters. The 'min' and 'max' arguments apply to the int and float variable types: they are optional, where the default 'min' value is 0 and the default 'max' value is, respectively, 100 for int types and 1 for float types. The 'p' argument applies to the bool variable type: this is the probability of the value being 'true'. The 'chars' argument applies to the char variable type: this consists of a string of semi-colon-separated characters, e.g. "a;c;f;g;z" from which the values will be uniformly picked from.
- [array:str] = getalledges(network = [var:network], layername = [str], *offset = [int(default:0)], *limit = [int(default:1000])
- Returns a JSON-ready array/vector of the edges that exist in the specified 1-mode layer and network. Will return maximum 1000 by default, but this can be changed with the 'limit' argument. Starts with the first one (index zero) by default, but this can be adjusted with 'offset'. Thus, if there are more than 1000 edges in the layer, all can be obtained either by using 'offset' for pagination and/or increasing the 'limit'.
- [array:str] = getallhyperedges(network = [var:network], layername = [str], *offset = [int(default:0)], *limit = [int(default:1000])
- Returns a JSON-ready array/vector of the hyperedge names that exist in the specified 2-mode layer and network. Will return maximum 1000 by default, but this can be changed with the 'limit' argument. Starts with the first one (index zero) by default, but this can be adjusted with 'offset'. Thus, if there are more than 1000 hyperedges, all can be obtained either by using 'offset' for pagination and/or increasing the 'limit'.
- [array:str] = getallnodes(structure = [var:structure], *offset = [int(default:0)], *limit = [int(default:1000])
- Returns a JSON-ready vector of the node ids of all nodes in the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure]. Will return maximum 1000 by default, but this can be changed with the 'limit' argument. Starts with the first node (index zero) by default, but this can be adjusted with 'offset'. Thus, if there are more than 1000 edges in the layer, all can be obtained either by using 'offset' for pagination and/or increasing the 'limit'. Do note that the node order reflects the order in which they were added, which thus may or may not be the same as their node ids. Also note that node attributes are not returned with this command.
- [str] = getattr(structure = [var:structure], nodeid = [uint], attrname = [str])
- Gets the value of the attribute 'attrname' for node 'nodeid' in the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure]. Note that the node attribute must first have been defined. Returns an empty string if the node has no value for this attribute.
- [str] = getattrsummary(structure = [var:structure], attrname = [str])
- Calculates and returns summary statistics for the specified node attribute in the nodeset. Statistics vary by attribute type: (int/float) Mean, Median, StdDev, Min, Max, Q1, Q3; (Bool) Count_true, Count_false, Ratio_true; (Char) Frequency distribution, Mode, Unique_values. All types include Count, Missing, and PercentageWithValue.
- [float] = getedge(network = [var:network], layername = [str], node1id = [uint], node2id = [uint])
- Returns the edge value between node1id (from) and node2id (to) in the specified layer 'layername', which can be either 1-mode or 2-mode. If the layer is 1-mode directional, node1id is the source and node2id is the destination. If no edge is found, returns zero. For 2-mode layers, the value represents the number of affiliations that the two nodes share in this particular layer, i.e. the value that typically emerge when using the classical matrix-multiplcation-approach for projecting 2-mode data to 1-mode.
- [array:uint] = gethyperedgenodes(network = [var:network], layername = [str], hypername = [str])
- Returns a JSON-ready array/vector of the node ids that are affiliated to the hyperedge with name 'hypername' in the specified layer and network. Note that the layer must be 2-mode.
- [uint] = getnbrnodes(structure = [var:structure])
- Get the number of nodes in the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure].
- [array:uint] = getnodealters(network = [var:network], nodeid = [uint], *layernames = [semicolon-separated layer names], *direction = ['both'(default),'in','out'], *unique = [false(default),true])
- Get the id of the alters to a specific node in the Network with the variable name [var:network]. Will either return the node alters for one or more specified layers (as given by layernames) or return alters in all layers. When specifying multiple layers, separate their names with a semicolon (;). Output is in standard JSON array format. By default, both in- and outbound ties are included in the set of alters, but this can be adjusted with the optional direction argument. When obtaining alters from multiple (or all) layers, the same alter node might appear in several layers. By default, such alter nodes will then appear multiple times in the returned array. However, by setting 'unique' to true, this array will be deduplicated before returned. (As a necessary side-effect of deduplication, the list will also be sorted, which it is not otherwise by design). Note: there is nothing stopping you from naming the same layer multiple times in the layernames string.
- [array:str] = getnodehyperedges(network = [var:network], layername = [str], nodeid = [uint])
- Returns a JSON-ready array/vector of the hyperedge names that a specified node is affiliated to in the specified layer and network. Note that the layer must be 2-mode.
- [uint] = getnodeidbyindex(structure = [var:structure], index = [int])
- Get the node id of the node with the specified index position in the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure]. Note that the index positions could change as nodes and node attributes are added and removed. Also note that nodes with attributes come first in the index, followed by nodes without attributes.
- [uint] = getrandomalter(network = [var:network], nodeid = [uint], *layername = [str], *direction = ['both'(default),'in','out'], *balanced = ['true','false'(default)])
- Get the node id of a random alter to the specified node. By default, both in- and outbound ties are considered, but this can be adjusted. By default, the pick is randomly picked among a specific layer as given by the 'layername' argument, or all available layers can be used by omitting this argument. If all layers are included, the 'balanced' argument specifies how the pick should be done. If balanced is set to 'true', a uniformly random pick between layer takes place first, followed by a random pick of an alter in the specific layer that was picked. If set to 'false', alters in all layers are first pooled together (with the possibility of an alter appearing multiple times) and a random pick is then done among this complete set of alters across layers.
- [int,int,float] = getrandomedge(network = [var:network], layername = [str], *maxattempts=[int(100)])
- Returns a random edge from the specified network and layer. While this is easy to do for 1-mode layers, the 2-mode layers samples random (pseudo-projected) node-to-node edges a bit differenlty. It first tries an approach where it randomly picks two nodes and checks if there is an edge between these two (i.e. if they share at least one affiliation). If this has been tried 100 times (adjusted with 'maxattempts') without finding an edge, the 2-mode heuristic then switches to a different kind of search that is slightly biased. First, it picks a random hyperedge based on their estimated projected weights, this being the size of the total graph that this hyperedge would generate if projected. Then it picks two random nodes from this hyperedge. The bias from this latter approach is that it is more likely to pick edges between nodes that share many affiliations.
- [uint] = getrandomnode(structure = [var:structure])
- Get a random node id from the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure].
- [str] = getwd()
- Returns the working directory that Threadle is currently using.
- help([str])
- Provides information about all available CLI commands, or detailed information about a specific command.
- [str] = i()
- Provides an inventory of the currently stored data objects. Note that the brackets can be ignored. (This command is in honor of all 1970's text adventure games, where 'i' was used to check what you were carrying).
- importlayer(network = [var:network], layername = [str], file = "[str]", format = ['edgelist','matrix'], *node1col = [int(default:0)], *node2col = [int(default:1)], *valuecol = [int(default:2)], *nodecol = [int(default:0)], *affcol = [int(default:1)], *header = ['true','false'(default)], *sep = [char(default:'\t')], *addmissingnodes = ['true','false'(default)])
- Imports data to an existing layer 'layername' in an existing network 'network' from file 'file'. The imported file is either in edgelist or matrix/table format, with the optional value-separating character given by 'sep' (defaults to tab). The 'addmissingnodes' boolean instructs what to do if encountering node id's that are not in the Nodeset: if set to true, these will be created and added to the Nodeset, if set to false, the relation will be ignored. Edgelists are assumed to be without headers but that can be adjusted with the 'header' argument. However, if a line can't be parsed as nodes and affiliations (as a header is), that's ok: it will just ignore. The matrix format assumes that the first row and first column are headers: this is compulsory in order to identify node ids (and affiliation names). For 1-mode layers and edgelist format, the first two column must contain node id's (for directional data, the first column is source node, and the second column is destination node). If the layer is for valued edges, a third column is expected that holds the value of ties. These columns can be adjusted with the 'node1col' and 'node2col' and, when applicable, the 'valuecol' arguments, where one can specify the column indexes to use (note that these start with 0). For 1-mode layers and matrix format, both rows and columns must contain node id's and the matrix must be square-shaped. For 2-mode layers and edgelist format, the first column contains the node id and the second column contains hyperedge labels (i.e. affiliations). This can be adjusted with the 'nodecol' and 'affcol' arguments (again, the first column has index 0). For 2-mode layers and matrix/table format, the first column (i.e. row headers) contain node ids, and the first row (i.e. column headers) contain hyperedge labels (i.e. affiliations).
- [str] = info(structure = [var:structure])
- Displays metadata about the structure, which is either a Nodeset or a Network. Name and Filepath are included for both structures. Network structures also provide data on existing relational layers, their properties and number of edges. Nodeset structures also provide data on number of nodes, and existing node attributes and their types.
- loadscript(file = "[str]")
- Loads and executes a script containing text CLI commands. Will abort if encountering an error.
- [var:structure] = loadfile(file = "[str]", type = ['nodeset','network'])
- Loads a structure from file 'file', using the internal text-based file format. The type of structure is given by the 'type' argument, which can be either 'nodeset' or 'network'. When loading a network file that refers to a nodeset file, the nodeset is also loaded. If the filepath has the ending .tsv, it is loaded in the standard internal text-based format, if the .tsv.gz is used, it is loading a gzipped version of this. If the filepath has the ending .bin, it is loaded as a Threadle-style binary file, if the .bin.gz is used, it is loading a gzipped version of this. Note that the .bin and .bin.gz format are very compact and not human-readable. Also: when loading a network file that refers to a nodeset file, the nodeset is also loaded.
- [str] = preview(structure = [var:structure])
- Previews the content of the structure with the variable name [var:structure]. Caps the number of outputted lines: max 10 nodes (with attributes) for nodesets, max 10 edges per 1-mode layer, and max 10 node-hyperedge affiliations per 2-mode layer.
- randomseed(*seed = [int(default:6031769)])
- Sets the random seed to the optionally specified integer value (defaults to 6031769). Used for reproducability.
- removeaff(network = [var:network], layername = [str], nodeid = [uint], hypername = [str])
- Removes the affiliation (if one exists) between node 'nodeid' and the hyperedge named 'hypername' in the specified layer 'layername' in network [var:network]. The specified layer must be 2-mode. Gives a warning if the node-hyperedge affiliation does not exist.
- removeattr(structure = [var:structure], nodeid = [uint], attrname = [str])
- Remove the attribute 'attrname' from the node with the id 'nodeid' in the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure]. Note that the node attribute must first have been defined. If the attribute is defined but not set for this node, this will return success though noting that the attribute was not set for this node.
- removeedge(network = [var:network], layername = [str], node1id = [uint], node2id = [uint])
- Removes the edge (if one exists) between node1id and node2id in the specified layer 'layername' (which must be 1-mode) in network [var:network]. If the layer is directional, node1id is the source (from) and node2id is the destination (to). Gives a warning message if any of the nodes are missing, or if the edge does not exist.
- removehyper(network = [var:network], layername = [str], hypername = [str])
- Removes the hyperedge 'hypername' from layer 'layername' in the specified network [var:network].
- removelayer(network = [var:network], layername = [str])
- Removes the relational layer 'layername' and all edges in that layer for the specified network.
- removenode(structure = [var:structure], nodeid = [uint])
- Removes the specified node from the the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure]. This CLI command will also iterate through all stored Network structures, removing related edges for the networks that use this Nodeset.
- savefile(structure = [var:structure], *file = "[str]")
- Saves the structure [var:structure] to file. By default, the Filepath of the structure is used to save to, but if the 'file' argument is provided, it will instead be saved to this, which will replace Filepath. If the Filepath property is not set (i.e. it hasn't been saved before, or loaded from file) and no 'file' is provided, an error will be returned. If the structure is a Network, this will also save the Nodeset to its file if it has been modified. However, if the Nodeset has not been saved before (or loaded from file), i.e. it has a null Filepath property, that will result in an error: you must then first save the Nodeset as a file before saving the Network. If the filename ends with .tsv, the structure is saved in tab-separated value format. With .tsv.gz, it is saved in tsv with additional Gzipped. If the file ending is .bin, the structure is saved in Threadle's own binary format, which is a compact, non-human-readable format. If the file ending is .bin.gz, it is the Gzipped version of the .bin format, which is typically quite compact and small.
- setattr(structure = [var:structure], nodeid = [uint], attrname = [str], attrvalue = [str])
- Sets the value of the attribute 'attrname' to 'attrvalue' for the node with the id 'nodeid' in the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure]. Note that the node attribute must first have been defined, and that the value of the attribute must be in accordance with the specific data type for which it was defined.
- setting(name = [str], value = ['true','false'])
- Changes the setting 'name' to either 'true' or 'false', i.e. either activating or deactivating it. Available settings are 'nodecache' (use node cache, lazy initialized), 'blockmultiedges' (prohibits the creation of multiple edges with identical connections and directions), 'onlyoutboundedges' (only stores outbound edges, i.e. no inbound edges, all to save memory for walker-only applications).
- setwd(dir = "[str,'~','~documents','~examples']")
- Sets the current working directory for Threadle to the relative or absolute path specified by 'dir'. There are three special options for this argument, options that work for all architectures. If set to '~', i.e. just the tilde character, the working directory will be set to the root user directory/folder. If set to '~documents', the working directory will be set to the user's documents folder (like the 'My Documents' folder in Windows). If a folder does not exist, an error is given.
- [int] = shortestpath(network = [var:network], node1id = [uint], node2id = [uint], *layername = [str])
- Calculates the shortest path from node1id to node2id in a network. Uses all layers unless a specific layer is specified. Note that shortest path measures are directional: for directional layers, the shortest path may indeed be different in the other direction. For symmetric layers, this is however moot.
- [var:network] = subnet(network = [var:network2], nodeset = [var:nodeset])
- Creates and stores a new network [var:network] that based on the provided network [var:network2] but only including the nodes in the provided nodeset [var:nodeset]. For instance, if a subset of a Nodeset has first been created using 'filter()', one can then use this command to create a subset of a network that is using the original Nodeset.
- symmetrize(network = [var:network], layername = [str], method = ['max'(default),'min','minnonzero','average','sum','product'], * newlayername = [str])
- Symmetrize the specified layer in the specific network. The layer must be 1-mode and preferably directional. A new, symmetrized version of the layer will be created: the original layer will remain as it is. An optional name for the new layer can be specified: otherwise, it will be automatically named.
- undefineattr(structure = [var:structure], attrname = [str])
- Removes the definition of a node attribute for the Nodeset (or the nodeset of the provided Network) that has the variable name [var:structure]. This will also iterate through all nodes with attributes, removing the attributes for those that have it.