Skip to content

Commit c28d146

Browse files
James CraigJames Craig
authored andcommitted
v506 [BENCHMARKED] - ECCC assimilation, :AnnualCycleStep, speedup, NOT_UPSTREAM_OF
Fully tested ECCC propagation/assimilation. (Assimilate.cpp;Model.h) -new routine AssimilationBackPropagate() (Solvers.cpp) -proper read :AssimilationMethod command (ParseInput.cpp) -write assimilation corrections (StandardOutput.cpp) Support :AnnualCycleStep input (TimeSeries.cpp) -Support provision of interpolation method for monthly data (Radiation.cpp;Reservoir.cpp;VegetationParams.cpp) -changed InterpolateMo (CommonFunctions.cpp;RavenInclude.h) Significant speedup for partially disabled models (100x speedup! for Athabasca) : -ignore non-zero gauge weights for disabled HRUs (ForcingGrid.cpp/.h;ParseTimeSeriesFile.cpp) new 'NOT_UPSTREAM_OF' Subbasin populate command (ParseHRUFile.cpp) cleanup -remove 'DEPTHRESHHOLD' spelling error support -dont include MULTIPLIERS in template file (ParsePropertyFile.cpp) -don't write first line of ForcingFunctions.csv (StandardOutput.cpp) QA/QC: -better zero time interval message (NetCDFReading.cpp) -check for valid subbasin property in multiplier (ParseHRUFile.cpp) -improved handling of bad subbasin in :OverrideStageDischargeCurve (ParseManagementFile.cpp) -check for WETLAND in :SoilProfiles warnings (ParsePropertyFile.cpp)
1 parent 709d7d5 commit c28d146

20 files changed

+275
-135
lines changed

src/Assimilate.cpp

Lines changed: 130 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ void CModel::InitializeDataAssimilation(const optStruct &Options)
8484
//
8585
void CModel::AssimilationOverride(const int p,const optStruct& Options,const time_struct& tt)
8686
{
87-
if(!Options.assimilate_flow) { return; }
87+
if (!Options.assimilate_flow) { return; }
88+
if (!_pSubBasins[p]->IsEnabled()){ return; }
8889

8990
//Get current, up-to-date flow and calculate scaling factor
9091
//---------------------------------------------------------------------
@@ -93,13 +94,14 @@ void CModel::AssimilationOverride(const int p,const optStruct& Options,const tim
9394
double Qobs,Qmod,Qmodlast;
9495
double alpha = _pGlobalParams->GetParams()->assimilation_fact;
9596

96-
Qobs = _aDAobsQ[p];
97-
Qmod = _pSubBasins[p]->GetOutflowRate();
97+
Qobs = _aDAobsQ[p];
98+
Qmod = _pSubBasins[p]->GetOutflowRate();
9899
Qmodlast= _pSubBasins[p]->GetLastOutflowRate();
99100
if(Qmod>PRETTY_SMALL) {
100101
_aDAscale [p]=1.0+alpha*((Qobs-Qmod)/Qmod); //if alpha = 1, Q=Qobs in observation basin
101-
//_aDAQadjust[p]=alpha*(Qobs-Qmod);//Option A: instantaneous flow
102-
_aDAQadjust[p]=alpha*(2.0*Qobs-Qmodlast-Qmod);//Option B: mean flow
102+
//_aDAQadjust[p]=alpha*(Qobs-Qmod);//Option A: instantaneous flow (should set second argument to AdjustAllFlows() to true)
103+
_aDAQadjust[p]=0.5*alpha*(2.0*Qobs-Qmodlast-Qmod);//Option B: mean flow
104+
//_aDAQadjust[p]=1.0;//TMP DEBUG - TESTING
103105
}
104106
else {
105107
_aDAscale [p]=1.0;
@@ -115,7 +117,9 @@ void CModel::AssimilationOverride(const int p,const optStruct& Options,const tim
115117
mass_added=_pSubBasins[p]->ScaleAllFlows(_aDAscale[p]/_aDAscale_last[p],_aDAoverride[p],Options.timestep,tt.model_time);
116118
}
117119
else if (Options.assim_method==DA_ECCC) {
118-
mass_added=_pSubBasins[p]->AdjustAllFlows(_aDAQadjust[p],_aDAoverride[p],Options.timestep,tt.model_time);
120+
if(_aDAoverride[p]){
121+
mass_added=_pSubBasins[p]->AdjustAllFlows(_aDAQadjust[p],true,Options.timestep,tt.model_time);
122+
}
119123
}
120124

121125
//
@@ -141,127 +145,188 @@ void CModel::PrepareAssimilation(const optStruct &Options,const time_struct &tt)
141145
double Qobs;
142146
double t_observationsOFF=ALMOST_INF;//Only used for debugging - keep as ALMOST_INF otherwise
143147

144-
int nn=(int)((tt.model_time+TIME_CORRECTION)/Options.timestep)+1;//end of timestep index
145-
146148
for(p=0; p<_nSubBasins; p++) {
147149
_aDAscale_last[p]=_aDAscale[p];
148150
_aDADrainSum[p]=0.0;
149151
}
150152

153+
int nn=(int)((tt.model_time+TIME_CORRECTION)/Options.timestep)+1;//end of timestep index
154+
151155
for(int pp=_nSubBasins-1; pp>=0; pp--)//downstream to upstream
152156
{
153157
p=GetOrderedSubBasinIndex(pp);
154158

155-
pdown=DOESNT_EXIST;
156-
if (_pSubBasins[p]->GetDownstreamID()!=DOESNT_EXIST){
157-
pdown = GetSubBasinByID(_pSubBasins[p]->GetDownstreamID())->GetGlobalIndex();
158-
}
159+
pdown=GetDownstreamBasin(p);
160+
159161
bool ObsExists=false; //observation available in THIS basin
160-
// observations in this basin, determine scaling variables based upon blank/not blank
161-
//----------------------------------------------------------------
162+
162163
if(_pSubBasins[p]->UseInFlowAssimilation())
163164
{
164165
for(int i=0; i<_nObservedTS; i++) //determine whether flow observation is available
165166
{
166167
if(IsContinuousFlowObs2(_pObservedTS[i],_pSubBasins[p]->GetID()))//flow observation is available and linked to this subbasin
167168
{
168169
Qobs = _pObservedTS[i]->GetSampledValue(nn); //mean timestep flow
169-
170-
//bool fakeblank=((tt.model_time>30) && (tt.model_time<40)) || ((tt.model_time>45) && (tt.model_time<47));//TMP DEBUG
171-
//if (fakeblank){Qobs=RAV_BLANK_DATA;}
172-
173-
if((Qobs!=RAV_BLANK_DATA) && (tt.model_time<t_observationsOFF))
174-
{
175-
//_aDAscale[p] calculated live in AssimilationOverride when up-to-date modelled flow available
176-
//same with _aDAQadjust[p]
177-
_aDAlength [p]=0.0;
178-
_aDAtimesince[p]=0.0;
179-
_aDAoverride [p]=true;
180-
_aDAobsQ [p]=Qobs;
181-
_aDADrainSum [p]=0.0; //??? maybe doesnt matter
182-
if (pdown != DOESNT_EXIST) {
183-
_aDADrainSum [pdown]+=_pSubBasins[p]->GetDrainageArea(); //DOES THIS HANDLE NESTING RIGHT?
184-
}
185-
}
186-
else
187-
{ //found a blank or zero flow value
188-
_aDAscale [p]=_aDAscale[p];//same adjustment as before - scaling persists
189-
_aDAQadjust [p]=_aDAQadjust[p];//same adjustment as before - flow magnitude persists
190-
_aDAtimesince[p]+=Options.timestep;
191-
_aDAlength [p]=0.0;
192-
_aDAoverride [p]=false;
193-
_aDAobsQ [p]=0.0;
194-
if (pdown != DOESNT_EXIST) {
195-
_aDADrainSum[pdown] += _aDADrainSum[p];
196-
}
197-
}
198170
ObsExists=true;
199171
break; //avoids duplicate observations
200172
}
201173
}
202174
}
203-
else {
204-
if (pdown != DOESNT_EXIST) {
205-
_aDADrainSum[pdown] += _aDADrainSum[p];
175+
176+
pdown=GetDownstreamBasin(p);
177+
178+
// observations in this basin, determine scaling variables based upon blank/not blank
179+
//----------------------------------------------------------------
180+
if (ObsExists) {
181+
if((Qobs!=RAV_BLANK_DATA) && (tt.model_time<t_observationsOFF))
182+
{
183+
//_aDAscale[p] calculated live in AssimilationOverride when up-to-date modelled flow available
184+
_aDAlength [p]=0.0;
185+
_aDAtimesince[p]=0.0;
186+
_aDAoverride [p]=true;
187+
_aDAobsQ [p]=Qobs;
188+
}
189+
else
190+
{ //found a blank or zero flow value
191+
_aDAscale [p]=_aDAscale[p];//same adjustment as before - scaling persists
192+
_aDAlength [p]=0.0;
193+
_aDAtimesince[p]+=Options.timestep;
194+
_aDAoverride [p]=false;
195+
_aDAobsQ [p]=0.0;
206196
}
207197
}
208198
// no observations in this basin, get scaling from downstream
209199
//----------------------------------------------------------------
210-
pdown=GetDownstreamBasin(p);
211-
if(ObsExists==false) { //observations may be downstream, propagate scaling upstream
200+
else if(!ObsExists) //observations may be downstream, propagate scaling upstream
201+
{
212202
//if ((pdown!=DOESNT_EXIST) && (!_aDAoverride[pdown])){ //alternate - allow information to pass through reservoirs
213-
if((pdown!=DOESNT_EXIST) && (_pSubBasins[p]->GetReservoir()==NULL) && (!_aDAoverride[pdown])) {
203+
if( (pdown!=DOESNT_EXIST) && (_pSubBasins[p]->GetReservoir()==NULL) && (!_aDAoverride[p]) && (_pSubBasins[p]->IsEnabled()) && (_pSubBasins[pdown]->IsEnabled())) {
214204
_aDAscale [p]= _aDAscale [pdown];
215-
_aDAQadjust [p]= _aDAQadjust [pdown] * (_pSubBasins[p]->GetDrainageArea() / _pSubBasins[pdown]->GetDrainageArea());
216205
_aDAlength [p]+=_pSubBasins [pdown]->GetReachLength();
217206
_aDAtimesince [p]= _aDAtimesince[pdown];
218-
_aDAoverride [p]=false;
207+
_aDAoverride [p]=false;
219208
}
220209
else{ //Nothing downstream or reservoir present in this basin, no assimilation
221210
_aDAscale [p]=1.0;
222-
_aDAQadjust [p]=0.0;
223211
_aDAlength [p]=0.0;
224212
_aDAtimesince[p]=0.0;
225213
_aDAoverride [p]=false;
226214
}
227215
}
228216
}// end downstream to upstream
229-
217+
218+
//Calculate _aDADrainSum, sum of assimilated drainage areas upstream of a subbasin outlet
219+
// and _aDADownSum, drainage of nearest assimilated flow observation
220+
// dynamic because data can disappear mid simulation
221+
//-------------------------------------------------------------------
222+
for(int pp=0;pp<_nSubBasins; pp++)
223+
{
224+
_aDADrainSum[p]=0.0;
225+
_aDADownSum [p]=0.0;
226+
}
230227
for(int pp=0;pp<_nSubBasins; pp++)//upstream to downstream
231228
{
232229
p=GetOrderedSubBasinIndex(pp);
230+
pdown=GetDownstreamBasin(p);
233231

234-
pdown=DOESNT_EXIST;
235-
if (_pSubBasins[p]->GetDownstreamID()!=DOESNT_EXIST){
236-
pdown = GetSubBasinByID(_pSubBasins[p]->GetDownstreamID())->GetGlobalIndex();
237-
}
238232
if (_aDAoverride[p]) {
239233
_aDADrainSum[p]=_pSubBasins[p]->GetDrainageArea();
240234
}
235+
/*
236+
else if (_pSubBasins[p]->GetReservoir()!=NULL){ //??
237+
_aDADrainSum[p]=0.0;
238+
}
239+
*/
241240
else if (pdown!=DOESNT_EXIST){
242-
_aDADrainSum[p]=_aDADrainSum[pdown];
241+
_aDADrainSum[pdown]+=_aDADrainSum[p];
243242
}
244243
}
244+
for(int pp=_nSubBasins-1;pp>=0; pp--)// downstream to upstream
245+
{
246+
p=GetOrderedSubBasinIndex(pp);
247+
pdown=GetDownstreamBasin(p);
245248

249+
if (_aDAoverride[p]) {
250+
_aDADownSum[p]=_pSubBasins[p]->GetDrainageArea();
251+
}
252+
else if (pdown!=DOESNT_EXIST){
253+
_aDADownSum[p]=_aDADownSum[pdown];
254+
}
255+
}
246256

247257
// Apply time and space correction factors
248258
//----------------------------------------------------------------
249259
double time_fact = _pGlobalParams->GetParams()->assim_time_decay;
250260
double distfact = _pGlobalParams->GetParams()->assim_upstream_decay/M_PER_KM; //[1/km]->[1/m]
251-
double ECCCwt;
252261
for(p=0; p<_nSubBasins; p++)
253262
{
254-
_aDAscale [p] =1.0+(_aDAscale[p]-1.0)*exp(-distfact*_aDAlength[p])*exp(-time_fact*_aDAtimesince[p]);
255-
//_aDAQadjust[p] =_aDAQadjust[p]*exp(-distfact*_aDAlength[p])*exp(-time_fact*_aDAtimesince[p]);
263+
_aDAscale[p] =1.0+(_aDAscale[p]-1.0)*exp(-distfact*_aDAlength[p])*exp(-time_fact*_aDAtimesince[p]);
264+
}
265+
}
266+
/////////////////////////////////////////////////////////////////
267+
/// \brief Calculates updated DA scaling coefficients for all subbasins for forthcoming timestep
268+
/// \note most scale coefficients will be 1.0 except:
269+
/// (1) gauges with valid observation data, which scale to override OR
270+
/// (2) basins at or upstream of missing observation data
271+
/// actual scaling performed in CModel::AssimilationOverride() during routing
272+
/// \param tt [in] current model time structure
273+
/// \param Options [in] current model options structure
274+
//
275+
void CModel::AssimilationBackPropagate(const optStruct &Options,const time_struct &tt)
276+
{
277+
int p,pdown;
256278

279+
if (!Options.assimilate_flow) {return;}
280+
281+
for(int pp=_nSubBasins-1; pp>=0; pp--)//downstream to upstream
282+
{
283+
p=GetOrderedSubBasinIndex(pp);
284+
285+
pdown=DOESNT_EXIST;
286+
if (_pSubBasins[p]->GetDownstreamID()!=DOESNT_EXIST){
287+
pdown = GetSubBasinByID(_pSubBasins[p]->GetDownstreamID())->GetGlobalIndex();
288+
}
289+
290+
// no observations in this basin, get scaling from downstream
291+
//----------------------------------------------------------------
292+
pdown=GetDownstreamBasin(p);
293+
long long SBID=_pSubBasins[p]->GetID();
294+
if(!_aDAoverride[p]) { //observations may be downstream, propagate scaling upstream
295+
if( (pdown!=DOESNT_EXIST) && (_pSubBasins[p]->GetReservoir()==NULL) && (!_aDAoverride[p]) && (_pSubBasins[p]->IsEnabled()) && (_pSubBasins[pdown]->IsEnabled())) {
296+
_aDAQadjust [p]= _aDAQadjust [pdown] * (_pSubBasins[p]->GetDrainageArea() / _pSubBasins[pdown]->GetDrainageArea());
297+
//cout<<"PROPAGATING "<<SBID<<": " <<setprecision(3)<< _aDAQadjust[p] << " from " << _aDAQadjust[pdown] << endl;
298+
}
299+
else{ //Nothing downstream or reservoir present in this basin, no assimilation
300+
_aDAQadjust [p]=0.0;
301+
}
302+
}
303+
else if (_pSubBasins[p]->IsEnabled()) {
304+
//cout<<"ASSIMILATING AT "<<SBID<<": " << _aDAQadjust[p] << endl;
305+
}
306+
} // end downstream to upstream
307+
308+
// Apply time and space correction factors
309+
//----------------------------------------------------------------
310+
double time_fact = _pGlobalParams->GetParams()->assim_time_decay;
311+
double distfact = _pGlobalParams->GetParams()->assim_upstream_decay/M_PER_KM; //[1/km]->[1/m]
312+
double ECCCwt;
313+
for(p=0; p<_nSubBasins; p++)
314+
{
315+
//_aDAQadjust[p] *=exp(-distfact*_aDAlength[p])*exp(-time_fact*_aDAtimesince[p]);
316+
ECCCwt=1.0;
257317
if (_aDADrainSum[p]!=0.0){
258318
ECCCwt = (_pSubBasins[p]->GetDrainageArea() - _aDADrainSum[p])/(_aDADownSum[p]-_aDADrainSum[p]);
259319
}
260-
else {
261-
ECCCwt=1.0;
262-
}
320+
263321
if (!_aDAoverride[p]) { //no scaling for stations being overridden, only upstream
264322
_aDAQadjust[p] = _aDAQadjust[p]*ECCCwt;
265323
}
266324
}
267-
}
325+
326+
// Actually update flows
327+
//----------------------------------------------------------------
328+
for(p=0; p<_nSubBasins; p++)
329+
{
330+
_pSubBasins[p]->AdjustAllFlows(_aDAQadjust[p],false,Options.timestep,tt.model_time);
331+
}
332+
}

src/CommonFunctions.cpp

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -856,11 +856,12 @@ string GetCurrentMachineTime(void)
856856
///
857857
/// \param aVal [in] Array of doubles representing monthly data
858858
/// \param &tt [in] Time structure which specifies interpolation
859-
/// \param &Options [in] Global model options information
859+
/// \param &method [in] Interpolation method
860860
/// \return Interpolated value at time denoted by &tt
861861
double InterpolateMo(const double aVal[12],
862862
const time_struct &tt,
863-
const optStruct &Options)
863+
const monthly_interp &method,
864+
const optStruct &Options)
864865
{
865866
double wt;
866867
int leap(0),mo,nextmo;
@@ -870,11 +871,11 @@ double InterpolateMo(const double aVal[12],
870871
month =tt.month;
871872
year =tt.year;
872873

873-
if (Options.month_interp==MONTHINT_UNIFORM)//uniform over month
874+
if (method==MONTHINT_UNIFORM)//uniform over month
874875
{
875876
return aVal[month-1];
876877
}
877-
else if (Options.month_interp==MONTHINT_LINEAR_FOM)//linear from first of month
878+
else if (method==MONTHINT_LINEAR_FOM)//linear from first of month
878879
{
879880
mo=month-1;
880881
nextmo=mo+1;
@@ -883,13 +884,13 @@ double InterpolateMo(const double aVal[12],
883884
wt=1.0-(double)(day)/(DAYS_PER_MONTH[mo]+leap);
884885
return wt*aVal[mo]+(1-wt)*aVal[nextmo];
885886
}
886-
else if ((Options.month_interp==MONTHINT_LINEAR_21) ||
887-
(Options.month_interp==MONTHINT_LINEAR_MID))
887+
else if ((method==MONTHINT_LINEAR_21) ||
888+
(method==MONTHINT_LINEAR_MID))
888889
//linear from 21st of month to 21st of next month (e.g., UBC_WM) or other day
889890
{
890891
double pivot=0.0;
891-
if (Options.month_interp==MONTHINT_LINEAR_21){pivot=21;}
892-
else if (Options.month_interp==MONTHINT_LINEAR_MID){
892+
if (method==MONTHINT_LINEAR_21){pivot=21;}
893+
else if (method==MONTHINT_LINEAR_MID){
893894
pivot=0.5*DAYS_PER_MONTH[month-1];
894895
if ((IsLeapYear(year,Options.calendar)) && (month==2)){pivot+=0.5;}
895896
}

src/ForcingGrid.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,7 @@ void CForcingGrid::SetGridDims(const int GridDims[3])
12321232
/// \param nHydroUnits number of HRUs
12331233
/// \param nGridCells number of grid cells
12341234
//
1235-
void CForcingGrid::SetIdxNonZeroGridCells(const int nHydroUnits, const int nGridCells, const optStruct &Options)
1235+
void CForcingGrid::SetIdxNonZeroGridCells(const int nHydroUnits, const int nGridCells, const bool *disabledHRUs,const optStruct &Options)
12361236
{
12371237
int row, col;
12381238
int minrow=_GridDims[1];
@@ -1249,7 +1249,7 @@ void CForcingGrid::SetIdxNonZeroGridCells(const int nHydroUnits, const int nGrid
12491249
if (_GridWeight != NULL){
12501250
for(int k=0; k<nHydroUnits; k++) { // loop over HRUs
12511251
for(int i=0; i<_nWeights[k]; i++) { // loop over all cells of NetCDF
1252-
if(_GridWeight[k][i] > 0.00001) {
1252+
if ((_GridWeight[k][i] > 0.00001) && (!disabledHRUs[k])){ // AND HRU not disabled!!!
12531253
nonzero[_GridWtCellIDs[k][i]] = true;
12541254
CellIdxToRowCol(_GridWtCellIDs[k][i],row,col);
12551255
if(row>maxrow) { maxrow=row; }

src/ForcingGrid.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*----------------------------------------------------------------
22
Raven Library Source Code
3-
Copyright (c) 2008-2024 the Raven Development Team
3+
Copyright (c) 2008-2025 the Raven Development Team
44
----------------------------------------------------------------*/
55
#ifndef FORCINGGRID_H
66
#define FORCINGGRID_H
@@ -188,7 +188,9 @@ class CForcingGrid //: public CForcingGridABC
188188
void SetAsPeriodEnding (); ///< set _period_ending
189189
void SetIs3D( const bool is3D); ///< set _is3D of class
190190
void SetIdxNonZeroGridCells( const int nHydroUnits,
191-
const int nGridCells, const optStruct &Options);
191+
const int nGridCells,
192+
const bool *disabledHRUs,
193+
const optStruct &Options);
192194
///< set _IdxNonZeroGridCells of class
193195
void CalculateChunkSize (const optStruct &Options);
194196

src/LandUseClass.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,6 @@ void CLandUseClass::SetSurfaceProperty(surface_struct &S,
481481
else if (!name.compare("DEP_MAX" )){S.dep_max =value;}
482482
else if (!name.compare("DEP_MAX_FLOW" )){S.dep_max_flow =value;}
483483
else if (!name.compare("DEP_N" )){S.dep_n =value;}
484-
else if (!name.compare("DEP_THRESHHOLD" )){S.dep_threshold =value;}/*old typo-backward compat*/
485484
else if (!name.compare("DEP_THRESHOLD" )){S.dep_threshold =value;}
486485
else if (!name.compare("DEP_CRESTRATIO" )){S.dep_crestratio =value;}
487486
else if (!name.compare("PDMROF_B" )){S.PDMROF_b =value; }
@@ -588,7 +587,6 @@ double CLandUseClass::GetSurfaceProperty(const surface_struct &S, string param_n
588587
else if (!name.compare("DEP_MAX" )){return S.dep_max ;}
589588
else if (!name.compare("DEP_MAX_FLOW" )){return S.dep_max_flow;}
590589
else if (!name.compare("DEP_N" )){return S.dep_n;}
591-
else if (!name.compare("DEP_THRESHHOLD" )){return S.dep_threshold;}/*old typo*/
592590
else if (!name.compare("DEP_THRESHOLD" )){return S.dep_threshold;}
593591
else if (!name.compare("DEP_K" )){return S.dep_k;}
594592
else if (!name.compare("DEP_SEEP_K" )){return S.dep_seep_k;}

src/Model.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,8 @@ class CModel: public CModelABC
544544
void AssimilationOverride (const int p,
545545
const optStruct &Options, const time_struct &tt);
546546
void PrepareAssimilation (const optStruct &Options, const time_struct &tt);
547+
void AssimilationBackPropagate (const optStruct &Options, const time_struct &tt);
548+
547549
void PrepareForcingPerturbation(const optStruct &Options, const time_struct &tt);
548550
void ApplyForcingPerturbation (const forcing_type f, force_struct &F, const int k, const optStruct& Options, const time_struct& tt);
549551

0 commit comments

Comments
 (0)