# nlogoxml.py # Copyright 2008 Richard Lawrence # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # The GPL is available at: http://www.gnu.org/licenses/gpl.html import math from xml.dom import minidom class SteppedVariable(object): """ Helper class to represent a steppedValueSet node. """ def __init__(self, node, chunk_size): "Initializes a SteppedVariable by introspecting a steppedValueSet node." # A little type checking if node.nodeName != "steppedValueSet": raise ValueError( "Tried to process a %s node as a SteppedVariable" % \ node.nodeName) # Someone has to tell us the chunk_size for this variable # ahead of time, since it's not in the XML. Non-positive # chunk sizes indicate that this variable should not be # chunked. if (chunk_size > 0): self._chunk_size = chunk_size else: self._chunk_size = None # Introspect the node to get the variable name and the range # of values it takes on. We convert numeric values to floats; # type conversion errors will propagate correctly. for key, val in node.attributes.items(): if key == 'variable': self._var_name = val elif key == 'first': self._first_val = float(val) elif key == 'last': self._last_val = float(val) elif key == 'step': self._step = float(val) else: raise ValueError( "Unexpected attribute '%s' on steppedValueSet node" % key) def _get_width_of_interval(self): "Returns the width of the variable's entire interval" return self._last_val - self._first_val def _get_number_of_chunks(self): "Returns the number of chunks in the variable's interval" if self._chunk_size: return int(math.ceil( self._get_width_of_interval() / self._chunk_size)) else: return 1 def _get_endpoints_for_chunk(self, i): "Returns the start and end values for a chunk" if self._chunk_size: start = self._first_val + (i * self._chunk_size) end = min(start + self._chunk_size, self._last_val) else: start = self._first_val end = self._last_val return start, end def get_nodes(self): "Returns a list of steppedValueSet nodes, one for each chunk." nodes = [] chunk_num = self._get_number_of_chunks() for i in range(chunk_num): new_node = minidom.Element(u"steppedValueSet") start, end = self._get_endpoints_for_chunk(i) new_node.setAttribute(u"variable", self._var_name) new_node.setAttribute(u"first", str(start)) new_node.setAttribute(u"last", str(end)) new_node.setAttribute(u"step", str(self._step)) nodes.append(new_node) return nodes def cartesian_product(L, *lists): """ Returns the a generator for the Cartesian product of {L, *lists}. Usage: rows = [(1, 2, 3), ('a', 'b', 'c'), (4, 5, 6, 7)] for tup in cartesian_product(*rows): do_something_with(tup) Courtesy of Kay Schluehr, http://bytes.com/forum/thread576730.html """ if not lists: for x in L: yield (x,) else: for x in L: for y in cartesian_product(lists[0],*lists[1:]): yield (x,)+y def split_experiments(doc, variable_chunk_sizes): """ Returns a new minidom.Document object with experiments split into chunks. doc should be a minidom.Document object representing a collection of NetLogo experiments. variable_chunk_sizes should be a dictionary with steppedValueSet variable names (as they appear in the XML) for keys, and positive numbers for values, e.g., {'community_size' : 10}. The new Document returned will have an node for each item in the Cartesian product of chunked steppedValueSet nodes for each variable. e.g., if there are 3 variables with 2 chunks each, the new Document will have 2*2*2 = 8 nodes. """ # To avoid mutating the doc, copy it first new_doc = doc.cloneNode(True) experiment_nodes = new_doc.getElementsByTagName("experiment") for e in experiment_nodes: stepped_val_nodes = e.getElementsByTagName("steppedValueSet") chunked_stepped_nodes = [] for s in stepped_val_nodes: # Take steppedValueNodes off the original experiment node; # this is so we can copy the experiment node and then # insert new steppedValueSet nodes e.removeChild(s) # Get the chunk size if available; if not, use -1 as an # indicator for SteppedVariable that there should only be # one chunk var_name = s.attributes['variable'].value chunk_size = variable_chunk_sizes.get(var_name, -1) # Get the chunked steppedValueSet nodes for the variable chunky = SteppedVariable(s, chunk_size) chunked_stepped_nodes.append(chunky.get_nodes()) # Free memory associated with s s.unlink() # Ok, so now chunked_stepped_nodes is a list of lists of # parentless steppedValueSet nodes...let's put them back in a # new experiment count = 0 for node_set in cartesian_product(*chunked_stepped_nodes): new_experiment_node = e.cloneNode(True) # Deep copy for node in node_set: new_experiment_node.appendChild(node) # put a count in the new experiment name before appending it count += 1 experiment_name = "%s-%d" % \ (e.attributes['name'].value, count) new_experiment_node.setAttribute('name', experiment_name) e.parentNode.appendChild(new_experiment_node) # Before we go, remove the (now empty-of-steppedValSets) # experiment node e.parentNode.removeChild(e) e.unlink() return new_doc