1from .. import fileio 2from ...weights import W 3 4__author__ = "Myunghwa Hwang <mhwang4@gmail.com>" 5__all__ = ["GeoBUGSTextIO"] 6 7 8class GeoBUGSTextIO(fileio.FileIO): 9 """Opens, reads, and writes weights file objects in the text format 10 used in `GeoBUGS <http://www.openbugs.net/Manuals/GeoBUGS/Manual.html>`_. 11 `GeoBUGS` generates a spatial weights matrix as an R object and writes 12 it out as an ASCII text representation of the R object. An exemplary 13 `GeoBUGS` text file is as follows. 14 15 ``` 16 list([CARD], [ADJ], [WGT], [SUMNUMNEIGH]) 17 ``` 18 19 where ``[CARD]`` and ``[ADJ]`` are required but the others are optional. 20 PySAL assumes ``[CARD]`` and ``[ADJ]`` always exist in an input text file. 21 It can read a `GeoBUGS` text file, even when its content is not written 22 in the order of ``[CARD]``, ``[ADJ]``, ``[WGT]``, and ``[SUMNUMNEIGH]``. 23 It always writes all of ``[CARD]``, ``[ADJ]``, ``[WGT]``, and ``[SUMNUMNEIGH]``. 24 PySAL does not apply text wrapping during file writing. 25 26 In the above example, 27 28 ``` 29 [CARD]: 30 num = c([a list of comma-splitted neighbor cardinalities]) 31 32 [ADJ]: 33 adj = c ([a list of comma-splitted neighbor IDs]) 34 35 If caridnality is zero, neighbor IDs are skipped. The ordering of 36 observations is the same in both ``[CARD]`` and ``[ADJ]``. 37 Neighbor IDs are record numbers starting from one. 38 39 [WGT]: 40 weights = c([a list of comma-splitted weights]) 41 The restrictions for [ADJ] also apply to ``[WGT]``. 42 43 [SUMNUMNEIGH]: 44 sumNumNeigh = [The total number of neighbor pairs] 45 the total number of neighbor pairs is an integer 46 value and the same as the sum of neighbor cardinalities. 47 ``` 48 49 Notes 50 ----- 51 52 For the files generated from R the ``spdep``, ``nb2WB``, and ``dput`` 53 functions. It is assumed that the value for the control parameter of 54 the ``dput`` function is ``NULL``. Please refer to R ``spdep`` and 55 ``nb2WB`` functions help files. 56 57 References 58 ---------- 59 60 * **Thomas, A., Best, N., Lunn, D., Arnold, R., and Spiegelhalter, D.** 61 (2004) GeoBUGS User Manual. R spdep nb2WB function help file. 62 63 """ 64 65 FORMATS = ["geobugs_text"] 66 MODES = ["r", "w"] 67 68 def __init__(self, *args, **kwargs): 69 args = args[:2] 70 fileio.FileIO.__init__(self, *args, **kwargs) 71 self.file = open(self.dataPath, self.mode) 72 73 def read(self, n=-1): 74 """Read a GeoBUGS text file. 75 76 Returns 77 ------- 78 w : libpysal.weights.W 79 A PySAL `W` object. 80 81 Examples 82 -------- 83 84 Type ``dir(w)`` at the interpreter to see what methods are supported. 85 Open a `GeoBUGS` text file and read it into a PySAL weights object. 86 87 >>> import libpysal 88 >>> w = libpysal.io.open( 89 ... libpysal.examples.get_path('geobugs_scot'), 'r', 'geobugs_text' 90 ... ).read() 91 92 Get the number of observations from the header. 93 94 >>> w.n 95 56 96 97 Get the mean number of neighbors. 98 99 >>> w.mean_neighbors 100 4.178571428571429 101 102 Get neighbor distances for a single observation. 103 104 >>> w[1] == dict({9: 1.0, 19: 1.0, 5: 1.0}) 105 True 106 107 """ 108 109 self._complain_ifclosed(self.closed) 110 111 w = self._read() 112 113 return w 114 115 def seek(self, pos) -> int: 116 if pos == 0: 117 self.file.seek(0) 118 self.pos = 0 119 120 def _read(self): 121 """Reads in a `GeoBUGSTextIO` object. 122 123 Raises 124 ------ 125 StopIteration 126 Raised at the EOF. 127 128 Returns 129 ------- 130 w : libpysal.weights.W 131 A PySAL `W` object. 132 133 """ 134 135 if self.pos > 0: 136 raise StopIteration 137 138 fbody = self.file.read() 139 body_structure = {} 140 141 for i in ["num", "adj", "weights", "sumNumNeigh"]: 142 i_loc = fbody.find(i) 143 144 if i_loc != -1: 145 body_structure[i] = (i_loc, i) 146 147 body_sequence = sorted(body_structure.values()) 148 body_sequence.append((-1, "eof")) 149 150 for i in range(len(body_sequence) - 1): 151 part, next_part = body_sequence[i], body_sequence[i + 1] 152 start, end = part[0], next_part[0] 153 part_text = fbody[start:end] 154 155 part_length, start, end = len(part_text), 0, -1 156 157 for c in range(part_length): 158 if part_text[c].isdigit(): 159 start = c 160 break 161 162 for c in range(part_length - 1, 0, -1): 163 if part_text[c].isdigit(): 164 end = c + 1 165 break 166 167 part_text = part_text[start:end] 168 part_text = part_text.replace("\n", "") 169 value_type = int 170 171 if part[1] == "weights": 172 value_type = float 173 174 body_structure[part[1]] = [value_type(v) for v in part_text.split(",")] 175 176 cardinalities = body_structure["num"] 177 adjacency = body_structure["adj"] 178 raw_weights = [1.0] * int(sum(cardinalities)) 179 180 if "weights" in body_structure and isinstance(body_structure["weights"], list): 181 raw_weights = body_structure["weights"] 182 183 no_obs = len(cardinalities) 184 neighbors = {} 185 weights = {} 186 pos = 0 187 188 for i in range(no_obs): 189 neighbors[i + 1] = [] 190 weights[i + 1] = [] 191 no_nghs = cardinalities[i] 192 193 if no_nghs > 0: 194 neighbors[i + 1] = adjacency[pos : pos + no_nghs] 195 weights[i + 1] = raw_weights[pos : pos + no_nghs] 196 197 pos += no_nghs 198 199 self.pos += 1 200 201 w = W(neighbors, weights) 202 203 return w 204 205 def write(self, obj): 206 """Writes a weights object to the opened text file. 207 208 Parameters 209 ---------- 210 obj : libpysal.weights.W 211 A PySAL `W` object. 212 213 Raises 214 ------ 215 TypeError 216 Raised when the input ``obj`` is not a PySAL `W`. 217 218 Examples 219 -------- 220 221 >>> import tempfile, libpysal, os 222 >>> testfile = libpysal.io.open( 223 ... libpysal.examples.get_path('geobugs_scot'), 'r', 'geobugs_text' 224 ... ) 225 >>> w = testfile.read() 226 227 Create a temporary file for this example. 228 229 >>> f = tempfile.NamedTemporaryFile(suffix='') 230 231 Reassign to a new variable. 232 233 >>> fname = f.name 234 235 Close the temporary named file. 236 237 >>> f.close() 238 239 Open the new file in write mode. 240 241 >>> o = libpysal.io.open(fname, 'w', 'geobugs_text') 242 243 Write the Weights object into the open file. 244 245 >>> o.write(w) 246 >>> o.close() 247 248 Read in the newly created text file. 249 250 >>> wnew = libpysal.io.open(fname, 'r', 'geobugs_text').read() 251 252 Compare values from old to new. 253 254 >>> wnew.pct_nonzero == w.pct_nonzero 255 True 256 257 Clean up the temporary file created for this example. 258 259 >>> os.remove(fname) 260 261 """ 262 263 self._complain_ifclosed(self.closed) 264 265 if issubclass(type(obj), W): 266 267 cardinalities, neighbors, weights = [], [], [] 268 for i in obj.id_order: 269 cardinalities.append(obj.cardinalities[i]) 270 neighbors.extend(obj.neighbors[i]) 271 weights.extend(obj.weights[i]) 272 273 self.file.write("list(") 274 self.file.write("num=c(%s)," % ",".join(map(str, cardinalities))) 275 self.file.write("adj=c(%s)," % ",".join(map(str, neighbors))) 276 self.file.write("sumNumNeigh=%i)" % sum(cardinalities)) 277 self.pos += 1 278 279 else: 280 raise TypeError("Expected a PySAL weights object, got: %s." % (type(obj))) 281 282 def close(self): 283 self.file.close() 284 fileio.FileIO.close(self) 285