22optimizer.py -- Optimization routines for PRMS parameters and data.
33'''
44from __future__ import print_function
5+ from itertools import tee , chain
56import pandas as pd
67import numpy as np
8+ import matplotlib .pyplot as plt
79import datetime as dt
8- import os , sys , json
10+ import os , sys , json , re
911
1012from copy import deepcopy
1113from numpy import log10
@@ -68,6 +70,9 @@ def __init__(self, parameters, data, control_file, working_dir,
6870 self .working_dir = working_dir
6971 self .title = title
7072 self .description = description
73+ self .srad_outputs = []
74+ self .measured_srad = None
75+ self .srad_hru = None
7176
7277 def srad (self , reference_srad_path , station_nhru , n_sims = 10 , method = '' ,\
7378 nproc = None ):
@@ -90,6 +95,12 @@ def srad(self, reference_srad_path, station_nhru, n_sims=10, method='',\
9095 Returns:
9196 (SradOptimizationResult)
9297 '''
98+ # assign the optimization object a copy of measured srad for plots
99+ self .measured_srad = pd .Series .from_csv (
100+ reference_srad_path , parse_dates = True
101+ )
102+
103+ self .srad_hru = station_nhru
93104
94105 srad_start_time = dt .datetime .now ()
95106 srad_start_time = srad_start_time .replace (second = 0 , microsecond = 0 )
@@ -118,7 +129,8 @@ def srad(self, reference_srad_path, station_nhru, n_sims=10, method='',\
118129 )
119130
120131 # run all scenarios
121- outputs = series .run (nproc = nproc ).outputs_iter ()
132+ outputs = list (series .run (nproc = nproc ).outputs_iter ())
133+ self .srad_outputs .extend (outputs )
122134
123135 srad_end_time = dt .datetime .now ()
124136 srad_end_time = srad_end_time .replace (second = 0 , microsecond = 0 )
@@ -128,72 +140,149 @@ def _error(x, y):
128140 ret = sum (ret )
129141 return ret
130142
131- measured_srad = pd .Series .from_csv (
132- reference_srad_path , parse_dates = True
133- )
134-
135143 srad_meta = {'stage' : 'swrad' ,
136- 'hru_id ' : station_nhru ,
144+ 'swrad_hru_id ' : self . srad_hru ,
137145 'optimization_title' : self .title ,
138146 'optimization_description' : self .description ,
139147 'start_time' : str (srad_start_time ),
140148 'end_time' : str (srad_end_time ),
141149 'measured_swrad' : reference_srad_path ,
142150 'sim_dirs' : [],
143- 'original_params' : self .parameters .base_file
151+ 'original_params' : self .parameters .base_file ,
152+ 'n_sims' : n_sims
144153 }
145154
146155 for output in outputs :
147156 srad_meta ['sim_dirs' ].append (output ['simulation_dir' ])
148-
149- json_outfile = OPJ (self .working_dir , '{0}_swrad_opt.json' .format (self .title ))
150-
157+
158+ json_outfile = OPJ (self .working_dir , _create_metafile_name (\
159+ self .working_dir , self .title , 'swrad' ))
160+
151161 with open (json_outfile , 'w' ) as outf :
152- json .dump (srad_meta , outf , sort_keys = True , indent = 4 , ensure_ascii = False )
162+ json .dump (srad_meta , outf , sort_keys = True , indent = 4 ,\
163+ ensure_ascii = False )
153164
154165 print ('{0}\n Output information sent to {1}\n ' .format ('-' * 80 , json_outfile ))
155166
156- # # calculate the top performing
157- # errors = (
158- # (
159- # output['simulation_dir'],
160- # _error(measured_srad,
161- # output['statvar']['swrad_' + str(station_nhru)])
162- # )
163- #
164- # for output in outputs
165- # )
166- #
167- # print("directory, error (swrad)")
168- # for directory, error in errors:
169- # print(directory, error) #
170- #
171- # monthly_errors = {str(mo): [] for mo in range(12)}
172- #
173- # for directory, error in errors:
174- #
175- # month, intcp, slope = (el.split(':')[1] for el in
176- # directory.split(os.sep)[-1].split('_'))
177- #
178- # monthly_errors[month].append((intcp, slope, error))
179- #
180- # rankings = {
181- # str(mo): list(sorted(monthly_errors[str(mo)], key=lambda x: x[-1]))
182- # for mo in range(12)
183- # }
184- #
185- # tops = [(mo, rankings[str(mo)][0]) for mo in range(12)]
186- #
187- # # update internal parameters
188- # for top in tops:
189- # mo = top[0]
190- # self.parameters['dday_intcp'][mo] = tops[mo][1][0]
191- # self.parameters['dday_slope'][mo] = tops[mo][1][1]
192- #
193- # return {
194- # 'best': tops,
195- # 'all': rankings
196- # }
167+ def plot_srad_optimization (self , freq = 'daily' , method = 'time_series' ):
168+ """
169+ Basic plotting of current srad optimization results with
170+ limited options for quick viewing, measured, original,
171+ and simulated swrad at the correspinding HRU is plotted
172+ either as time series (all three) or scatter (measured
173+ versus simulated). Not recommended for plotting results
174+ when n_sims is very high, use plotting options from
175+ an OptimizationResult object
176+
177+ Kwargs:
178+ freq (str): frequency of time series plots, value can be 'daily'
179+ or 'monthly' for solar radiation !!!need to finish monthly!!!
180+ method (str): 'time_series' for time series sub plot of each
181+ simulation alongside measured radiation. Other choice is
182+ 'correlation' which plots each measured daily solar radiation
183+ value versus the corresponding simulated variable as subplots
184+ one for each simulation in the optimization. With coefficients
185+ of determiniationi i.e. square of pearson correlation coef.
186+ """
187+ if not self .srad_outputs :
188+ raise ValueError ('You have not run any srad optimizations' )
189+
190+ # indices that measured and simulated swrad share (the intersection)
191+ X = self .measured_srad
192+ idx = X .index .intersection (self .srad_outputs [0 ]['statvar' ]\
193+ ['swrad_{}' .format (self .srad_hru )].index )
194+ X = X [idx ]
195+ n = len (self .srad_outputs ) # number of simulations to plot
196+
197+ if freq == 'daily' and method == 'time_series' :
198+ fig , ax = plt .subplots (n , sharex = True , sharey = True ,\
199+ figsize = (12 ,n * 3.5 ))
200+ axs = ax .ravel ()
201+ for i ,out in enumerate (self .srad_outputs ):
202+ axs [i ].plot (out ['statvar' ]['swrad_{}' .format (self .srad_hru )]\
203+ [idx ], 'r.' , markersize = 3 , label = 'Simulated' )
204+ axs [i ].plot (self .measured_srad [idx ], 'k.' , markersize = 3 ,\
205+ label = 'Measured' )
206+ axs [i ].set_ylabel ('sim: {}' .format (out ['simulation_dir' ].\
207+ split (os .sep )[- 1 ].replace ('_' , ' ' )))
208+ if i == 0 : axs [i ].legend (markerscale = 5 , loc = 'best' )
209+ fig .subplots_adjust (hspace = 0 )
210+ fig .autofmt_xdate ()
211+ plt .show ()
212+
213+ elif method == 'correlation' :
214+ ## number of subplots and rows (two plots per row)
215+ nrow = n // 2 # round down if odd n
216+ ncol = 2
217+ odd_n = False
218+ if n / 2. - nrow == 0.5 :
219+ nrow += 1 # odd number need extra row
220+ odd_n = True
221+ ## figure
222+ fig , ax = plt .subplots (nrows = nrow , ncols = ncol , figsize = (12 ,n * 3 ))
223+ axs = ax .ravel ()
224+ ## subplot dimensions
225+ meas_min = min (X )
226+ meas_max = max (X )
227+
228+ for i , out in enumerate (self .srad_outputs ):
229+ Y = out ['statvar' ]['swrad_{}' .format (self .srad_hru )][idx ]
230+ sim_max = max (Y )
231+ sim_min = min (Y )
232+ m = max (meas_max ,sim_max )
233+ axs [i ].plot ([0 , m ], [0 , m ], 'k--' , lw = 2 ) ## one to one line
234+ axs [i ].set_xlim (meas_min ,meas_max )
235+ axs [i ].set_ylim (sim_min , sim_max )
236+ axs [i ].scatter (X , Y , facecolors = 'none' , edgecolor = 'r' , s = 3 )
237+ axs [i ].set_ylabel ('sim: {}' .format (out ['simulation_dir' ]\
238+ .split (os .sep )[- 1 ].replace ('_' , ' ' )))
239+ axs [i ].set_xlabel ('Measured shortwave radiation' )
240+ axs [i ].text (0.05 , 0.95 ,r'$R^2 = {0:.2f}$' .format (\
241+ X .corr (Y )** 2 ), fontsize = 16 ,\
242+ ha = 'left' , va = 'center' , transform = axs [i ].transAxes )
243+ if odd_n : # empty subplot if odd number of simulations
244+ fig .delaxes (axs [n ])
245+
246+ def _create_metafile_name (out_dir , opt_title , stage ):
247+ """
248+ Search through output directory where simulations are conducted
249+ look for all metadata simulation json files and find out if the
250+ current simulation is a replicate. Then use that information to
251+ build the correct file name for the output json file. The series
252+ are typically run in parallelel that is why this step has to be
253+ done after running multiple simulations from an optimization stage.
254+
255+ Args:
256+ out_dir (str): path to directory with model results, i.e.
257+ location where simulation series outputs and optimization
258+ json files are located, aka Optimizer.working_dir
259+ opt_title (str): optimization instance title for file search
260+ stage (str): stage of optimization, e.g. 'swrad', 'pet'
261+
262+ Returns:
263+ name (str): file name for the current optimization simulation series
264+ metadata json file. E.g 'dry_creek_swrad_opt.json', or if
265+ this is the second time you have run an optimization titled
266+ 'dry_creek' the next json file will be returned as
267+ 'dry_creek_swrad_opt1.json' and so on with integer increments
268+ """
269+ swrad_meta_re = re .compile (r'^{}_{}_opt(\d*)\.json' .format (opt_title , stage ))
270+ reps = []
271+ for f in os .listdir (out_dir ):
272+ if swrad_meta_re .match (f ):
273+ nrep = swrad_meta_re .match (f ).group (1 )
274+ if nrep == '' :
275+ reps .append (0 )
276+ else :
277+ reps .append (nrep )
278+
279+ if not reps :
280+ name = '{}_{}_opt.json' .format (opt_title , stage )
281+ else :
282+ # this is the nth optimization done under the same title
283+ n = max (map (int , reps )) + 1
284+ name = '{}_{}_opt{}.json' .format (opt_title , stage , n )
285+ return name
197286
198287def _resample_param (param , p_min , p_max , noise_factor = 0.1 ):
199288 """
@@ -204,10 +293,11 @@ def _resample_param(param, p_min, p_max, noise_factor=0.1 ):
204293 to each parameter element by adding a RV from a normal distribution
205294 with mean 0, sigma = param allowable range / 10.
206295
207- Arguments :
296+ Args :
208297 param (numpy.ndarray): ndarray of parameter to be resampled
209298 p_min (float): lower bound of PRMS allowable range for param
210299 p_max (float): upper bound of PRMS allowable range for param
300+ Kwargs:
211301 noise_factor (float): factor to multiply parameter range by,
212302 use the result as the standard deviation for the normal rand.
213303 variable used to add element wise noise. i.e. higher
@@ -220,7 +310,7 @@ def _resample_param(param, p_min, p_max, noise_factor=0.1 ):
220310
221311 low_bnd = p_min - np .min (param ) # lowest param value minus allowable min
222312 up_bnd = p_max - np .max (param )
223- s = (p_max - p_min ) * noise_factor # default is one tenth rangee (0.1)
313+ s = (p_max - p_min ) * noise_factor # stddev noise, default: range*(1/10)
224314
225315 shifted_param = np .random .uniform (low = low_bnd , high = up_bnd ) + param
226316 ## add noise to each point keeping result within allowable range
@@ -255,27 +345,30 @@ def get_optr_jsons(self, work_dir, stage):
255345 of corresponding json file paths for each stage as values.
256346
257347 Arguments:
258- work_dir (str): path to simulation directory where
259- optimization simulations were conducted and where
260- corresponding json files should exist.
348+ work_dir (str): path to directory with model results, i.e.
349+ location where simulation series outputs and optimization
350+ json files are located, aka Optimizer.working_dir
261351 stage (str): the stage ('swrad', 'pet', 'flow', etc.) of
262352 the optimization in which to gather the jsons, if
263353 stage is 'all' then each stage will be gathered.
264354 Returns:
265355 ret (dict): dictionary of stage (keys) and lists of
266356 json file paths for that stage (values).
267357 """
268-
358+
269359 ret = {}
270360 if stage != 'all' :
361+ optr_metafile_re = re .compile (r'^.*_{}_opt(\d*)\.json' .format (stage ))
271362 ret [stage ] = [OPJ (work_dir , f ) for f in \
272363 os .listdir (work_dir ) if \
273- f . endswith ( '_{0}_opt.json' . format ( stage ) ) ]
364+ optr_metafile_re . match ( f ) ]
274365 else :
275366 stages = ['swrad' , 'pet' , 'flow' ]
276367 for s in stages :
277- ret [s ] = self .get_optr_jsons (work_dir , s )
278-
368+ optr_metafile_re = re .compile (r'^.*_{}_opt(\d*)\.json' .format (s ))
369+ ret [s ] = [OPJ (work_dir , f ) for f in \
370+ os .listdir (work_dir ) if \
371+ optr_metafile_re .match (f ) ]
279372 return ret
280373
281374class SradOptimizationResult (OptimizationResult ):
0 commit comments