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