Interface to OptiPass.exe (the command line version of OptiPass)
An instance of the OP class encapsulates all the information
related to a single optimization run. The constructor, called
from the GUI, is passed the options selected by the user (budget
levels, restoration targets, etc). Methods of the class set up
and run an optimization based on these options:
An OP object can also be instantiated by the command line API in
main.py. When run on macOS or Linux it can be used to test the
functions that creates the OP input file and parse the results.
When run on a Windows system it can also run OptiPass.
| Parameters: |
-
project
(Project)
–
a Project object containing barrier file
-
regions
(list[str])
–
a list of region names from the barrier file
-
targets
(list[str])
–
a list of 2-letter target IDs
-
weights
(list[str])
–
optional list of integer weights for each target
-
climate
(str)
–
either 'Current' or 'Future'
|
Source code in src/tidegates/optipass.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 | def __init__(self, project: Project, regions: list[str], targets: list[str], weights: list[str], climate: str):
'''
Instantiate a new OP object.
Arguments:
project: a Project object containing barrier file
regions: a list of region names from the barrier file
targets: a list of 2-letter target IDs
weights: optional list of integer weights for each target
climate: either 'Current' or 'Future'
'''
self.project = project
self.regions = regions
if weights:
self.weights = [int(s) for s in weights]
self.weighted = True
else:
self.weights = [1] * len(targets)
self.weighted = False
self.climate = climate
structs = self.project.targets[climate] if climate else self.project.targets
self.targets = [structs[t] for t in targets]
self.input_frame = None
self.outputs = None
|
Create a data frame that will be written in the format of a "barrier
file" that will be read by OptiPass. Save the frame as the
input_frame attribute of the object.
src/tidegates/optipass.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108 | def generate_input_frame(self):
'''
Create a data frame that will be written in the format of a "barrier
file" that will be read by OptiPass. Save the frame as the
input_frame attribute of the object.
'''
filtered = self.project.data[self.project.data.REGION.isin(self.regions)]
filtered.index = list(range(len(filtered)))
df = filtered[['BARID','REGION']]
header = ['ID','REG']
df = pd.concat([df, pd.Series(np.ones(len(filtered)), name='FOCUS', dtype=int)], axis=1)
header.append('FOCUS')
df = pd.concat([df, filtered['DSID']], axis=1)
header.append('DSID')
for t in self.targets:
df = pd.concat([df, filtered[t.habitat]], axis=1)
header.append('HAB_'+t.abbrev)
for t in self.targets:
df = pd.concat([df, filtered[t.prepass]], axis=1)
header.append('PRE_'+t.abbrev)
df = pd.concat([df, filtered['NPROJ']], axis=1)
header.append('NPROJ')
df = pd.concat([df, pd.Series(np.zeros(len(filtered)), name='ACTION', dtype=int)], axis=1)
header.append('ACTION')
df = pd.concat([df, filtered['COST']], axis=1)
header += ['COST']
for t in self.targets:
df = pd.concat([df, filtered[t.postpass]], axis=1)
header.append('POST_'+t.abbrev)
df.columns = header
self.input_frame = df
return df
|
run(budgets, preview)
Generate and execute the shell commands that run OptiPass. If the shell
environment includes a variable named WINEARCH it means the script is
running on Linux, and we need to use Wine, otherwise build a command that
will run on Windows.
The first time OptiPass is run it will be given a budget of $0 to establish
the current passage levels. It's then run once more at each level in the
budgets list.
Each time OptiPass is run it is passed the same input file, but it will
write outputs to a separate file that includes the budget level in the file name.
The list of output file names is saved in an instance variable.
| Parameters: |
-
budgets
(list[int])
–
a list of budget values (dollar amounts)
-
preview
(bool)
–
if True, print shell commands but don't execute them
|
src/tidegates/optipass.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168 | def run(self, budgets: list[int], preview: bool):
'''
Generate and execute the shell commands that run OptiPass. If the shell
environment includes a variable named WINEARCH it means the script is
running on Linux, and we need to use Wine, otherwise build a command that
will run on Windows.
The first time OptiPass is run it will be given a budget of $0 to establish
the current passage levels. It's then run once more at each level in the
budgets list.
Each time OptiPass is run it is passed the same input file, but it will
write outputs to a separate file that includes the budget level in the file name.
The list of output file names is saved in an instance variable.
Arguments:
budgets: a list of budget values (dollar amounts)
preview: if True, print shell commands but don't execute them
'''
if platform.system() == 'Windows':
app = 'bin\\OptiPassMain.exe'
elif platform.system() == 'Linux' and os.environ.get('WINEARCH'):
app = 'wine bin/OptiPassMain.exe'
else:
Logging.log(f'{platform.system()} not configured to run WINE')
self.outputs = None
return
template = app + ' -f {bf} -o {of} -b {n}'
df = self.generate_input_frame()
_, barrier_file = tempfile.mkstemp(suffix='.txt', dir='./tmp', text=True)
df.to_csv(barrier_file, index=False, sep='\t', lineterminator=os.linesep, na_rep='NA')
self.budget_max, self.budget_delta = budgets
num_budgets = self.budget_max // self.budget_delta
outputs = []
root, _ = os.path.splitext(barrier_file)
for i in range(num_budgets + 1):
outfile = f'{root}_{i+1}.txt'
budget = self.budget_delta * i
cmnd = template.format(bf=barrier_file, of=outfile, n=budget)
if (num_targets := len(self.targets)) > 1:
cmnd += ' -t {}'.format(num_targets)
cmnd += ' -w ' + ', '.join([str(n) for n in self.weights])
Logging.log(cmnd)
print(cmnd)
if not preview:
res = subprocess.run(cmnd, shell=True, capture_output=True)
print(res.stdout)
print(res.stderr)
if preview or (res.returncode == 0):
outputs.append(outfile)
# progress_hook()
else:
Logging.log('OptiPass failed:')
Logging.log(res.stderr)
self.outputs = outputs
|
collect_results(scaled=False)
Parse the output files produced by OptiPass (the file names are in
self.outputs) and collect the results, which are saved in two Pandas
data frames.
src/tidegates/optipass.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198 | def collect_results(self, scaled=False):
'''
Parse the output files produced by OptiPass (the file names are in
self.outputs) and collect the results, which are saved in two Pandas
data frames.
'''
df = self.input_frame
G = nx.from_pandas_edgelist(
df[df.DSID.notnull()],
source='ID',
target='DSID',
create_using=nx.DiGraph
)
for x in df[df.DSID.isnull()].ID:
G.add_node(x)
self.paths = { n: self._path_from(n,G) for n in G.nodes }
cols = { x: [] for x in ['budget', 'habitat', 'gates']}
for fn in self.outputs:
self._parse_op_output(fn, cols)
self.summary = pd.DataFrame(cols)
dct = {}
for i in range(len(self.summary)):
b = int(self.summary.budget[i])
dct[b] = [ 1 if g in self.summary.gates[i] else 0 for g in self.input_frame.ID]
self.matrix = pd.DataFrame(dct, index=self.input_frame.ID)
self.matrix['count'] = self.matrix.sum(axis=1)
self.potential_habitat(self.targets, scaled)
|
_path_from(x, graph)
Return a list of nodes in the path from a barrier to a downstream barrier that
has no descendants.
| Parameters: |
-
x
–
the barrier at the start of the path
-
graph
–
the digraph with barrier connectivity
|
src/tidegates/optipass.py
200
201
202
203
204
205
206
207
208
209 | def _path_from(self, x, graph):
'''
Return a list of nodes in the path from a barrier to a downstream barrier that
has no descendants.
Arguments:
x: the barrier at the start of the path
graph: the digraph with barrier connectivity
'''
return [x] + [child for _, child in nx.dfs_edges(graph,x)]
|
_parse_op_output(fn, dct)
Parse an output file, appending results to the lists. We need to handle
two different formats, depending on whether there was one target or more
than one.
| Parameters: |
-
fn
–
the name of the file to parse
-
dct
–
a dictionary of column names, results are appended to lists in this dictionary
|
src/tidegates/optipass.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258 | def _parse_op_output(self, fn, dct):
'''
Parse an output file, appending results to the lists. We need to handle
two different formats, depending on whether there was one target or more
than one.
Arguments:
fn: the name of the file to parse
dct: a dictionary of column names, results are appended to lists in this dictionary
'''
def parse_header_line(line, tag):
tokens = line.strip().split()
if not tokens[0].startswith(tag):
return None
return tokens[1]
with open(fn) as f:
amount = parse_header_line(f.readline(), 'BUDGET')
dct['budget'].append(float(amount))
if parse_header_line(f.readline(), 'STATUS') == 'NO_SOLN':
raise RuntimeError('No solution')
f.readline() # skip OPTGAP
line = f.readline()
if line.startswith('PTNL'):
# dct['weights'].append([1.0])
hab = parse_header_line(line, 'PTNL_HABITAT')
dct['habitat'].append(float(hab))
f.readline() # skip NETGAIN
else:
lst = []
while w := parse_header_line(f.readline(), 'TARGET'):
lst.append(float(w))
# dct['weights'].append(lst)
while line := f.readline(): # skip the individual habitat lines
if line.startswith('WT_PTNL_HAB'):
break
hab = parse_header_line(line, 'WT_PTNL_HAB')
dct['habitat'].append(float(hab))
f.readline() # skip WT_NETGAIN
f.readline() # skip blank line
f.readline() # skip header
lst = []
while line := f.readline():
name, action = line.strip().split()
if action == '1':
lst.append(name)
dct['gates'].append(lst)
|
potential_habitat(tlist, scaled)
Compute the potential habitat available before and after restoration, using
the original unscaled habitat values.
| Parameters: |
-
tlist
–
-
scaled
–
True if we should create weighted potential habitat values
|
src/tidegates/optipass.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286 | def potential_habitat(self, tlist, scaled):
'''
Compute the potential habitat available before and after restoration, using
the original unscaled habitat values.
Arguments:
tlist: list of target IDs
scaled: True if we should create weighted potential habitat values
'''
filtered = self.project.data[self.project.data.REGION.isin(self.regions)].fillna(0)
filtered.index = filtered.BARID
wph = np.zeros(len(self.summary))
for i in range(len(tlist)):
t = self.targets[i]
cp = self._ah(t, filtered, scaled)
wph += (self.weights[i] * cp)
col = pd.DataFrame({t.abbrev: cp})
self.summary = pd.concat([self.summary, col], axis=1)
if not scaled:
gain = self._gain(t, filtered)
self.matrix = pd.concat([self.matrix, filtered[t.unscaled], gain], axis=1)
# If scaled is True add the wph column so we can compare with OP values
if scaled:
self.summary = pd.concat([self.summary, pd.DataFrame({'wph': wph})], axis = 1)
self.summary['netgain'] = self.summary.habitat - self.summary.habitat[0]
return self.summary
|
_ah(target, data, scaled)
Compute the available habitat for a target, in the form of
a vector of habitat values for each budget level;
| Parameters: |
-
target
–
a Target object (with ID and names of data columns to use)
-
data
–
-
scaled
–
if True is the scaled benefit column
|
src/tidegates/optipass.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307 | def _ah(self, target, data, scaled):
"""
Compute the available habitat for a target, in the form of
a vector of habitat values for each budget level;
Arguments:
target: a Target object (with ID and names of data columns to use)
data: the barrier dataframe
scaled: if True is the scaled benefit column
"""
budgets = self.summary.budget
m = self.matrix
res = np.zeros(len(budgets))
for i in range(len(res)):
action = m.iloc[:,i]
pvec = data[target.postpass].where(action == 1, data[target.prepass])
habitat = data[target.habitat if scaled else target.unscaled]
res[i] = sum(prod(pvec[x] for x in self.paths[b]) * habitat[b] for b in m.index)
return res
|
table_view(test=False)
Create a table that will be displayed by the GUI
src/tidegates/optipass.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355 | def table_view(self, test=False):
'''
Create a table that will be displayed by the GUI
'''
filtered = self.project.data[self.project.data.REGION.isin(self.regions)]
filtered = filtered.set_index('BARID')
if test:
info_cols = other_cols = { }
else:
info_cols = {
'REGION': 'Region',
'BarrierType': 'Type',
'DSID': 'DSID',
'COST': 'Cost',
}
other_cols = {
'PrimaryTG': 'Primary',
'DominantTG': 'Dominant',
'POINT_X': 'Longitude',
'POINT_Y': 'Latitude',
}
budget_cols = OP.format_budgets([c for c in self.matrix.columns if isinstance(c,int) and c > 0])
df = pd.concat([
filtered[info_cols.keys()].rename(columns=info_cols),
self.matrix.rename(columns=budget_cols),
filtered[other_cols.keys()].rename(columns=other_cols),
], axis=1)
dct = { t.unscaled: t.short+'_hab' for t in self.targets }
dct |= { f'GAIN_{t.abbrev}': t.short+'_gain' for t in self.targets }
df = df.rename(columns=dct)
del df[0]
df = df[df['count'] > 0].sort_values(by='count', ascending=False).fillna('-')
df = df.rename(columns={'count': 'Count'})
df = df.reset_index(names=['ID'])
# df.columns = pd.MultiIndex.from_tuples(df.columns)
return df
|
make_roi_curves()
Generate ROI plots based on computed benefits.
src/tidegates/optipass.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408 | def make_roi_curves(self):
"""
Generate ROI plots based on computed benefits.
"""
figures = []
download_figures = []
climate = None
subtitle = 'Region: ' if len(self.regions) == 1 else 'Regions: '
subtitle += ', '.join(self.regions)
for i, t in enumerate(self.targets):
title = t.long
if t.infra:
climate = self.climate
title += f' ({climate} Climate)'
if self.weighted:
title += f' ⨉ {int(self.weights[i])}'
f = self.bokeh_figure(self.summary.budget, self.summary[t.abbrev], title, subtitle, t.label)
figures.append((t.short, f))
f = self.pyplot_figure(self.summary.budget, self.summary[t.abbrev], title, subtitle, t.label)
download_figures.append((t.short, f))
if len(self.targets) > 1:
title = 'Combined Potential Benefit'
if climate:
title += f' ({climate} Climate)'
f = self.bokeh_figure(self.summary.budget, self.summary.netgain, title, subtitle, 'Weighted Net Gain')
figures.insert(0, ('Net', f))
f = self.pyplot_figure(self.summary.budget, self.summary[t.abbrev], title, subtitle, 'Weighted Net Gain')
download_figures.insert(0, ('Net', f))
self.display_figures = figures
self.download_figures = download_figures
|