modules/analyze/components/hydro.js

  1. import * as stats from "./stats.js";
  2. /**
  3. * Main class used for hydrological analyses.
  4. * @class
  5. * @name hydro
  6. */
  7. export default class hydro {
  8. // constructor(){
  9. // //Defining stats variable to be used throughout the component
  10. // this.stats = new stats()
  11. // }
  12. /**
  13. * Computation of aereal mean precipitation for a river basin given it has 2 or more different stations.
  14. * @method arithmetic
  15. * @memberof hydro
  16. * @param {Object} data - Contains: array object with precipitation with equal amounts of data from different rain gauges.
  17. * @returns {Object} Array with object with average precipitation for a specific time series.
  18. * @example
  19. * hydro.analyze.hydro.arithmetic({data: [[1, 2, 3, 4], [2, 3, 5, 2]]});
  20. */
  21. static arithmetic({ params, args, data } = {}) {
  22. var arr = data,
  23. average = [],
  24. final = [],
  25. n = arr.length;
  26. for (var i = 0; i < arr.length; i++) {
  27. for (var j = 0; j < arr[0].length; j++) {
  28. average.push(arr[i][j]);
  29. }
  30. }
  31. for (var h = 0; h < average.length; h = +n) {
  32. var minarr = average.slice(h, h + n);
  33. final[h] = this.totalprec(minarr) / minarr.length;
  34. var filtered = final.filter(function (el) {
  35. return el != null;
  36. });
  37. }
  38. return filtered;
  39. }
  40. /**Thiessen polygon method for rainfall areal averaging.
  41. * Calculates the weighted average of point rainfall observations by dividing the study area into polygonal subareas and weighting each observation by the proportion of the total area it contributes to.
  42. * @method thiessen
  43. * @memberof hydro
  44. * @param {Object} params - Contains: areas (array with the areas of each polygon)
  45. * @param {Object} args - Empty for now
  46. * @param {Object} data - Contains: an array of arrays with rainfall observations for each point
  47. * @returns {Array} - Array with the weighted average of point rainfall observations for each time step
  48. * @example
  49. * hydro.analyze.thiessen({params: {areas: [10, 20, 30]}data: [[0.3, 0.2, 0.5], [0.2, 0.3, 0.4], [0.1, 0.4, 0.5]]});
  50. */
  51. static thiessen({ params, args, data } = {}) {
  52. const areas = params.areas;
  53. // Check if number of data arrays matches number of areas, fill remaining with empty arrays
  54. const numAreas = areas.length;
  55. const numDataArrays = data.length;
  56. if (numDataArrays < numAreas) {
  57. const emptyArrays = Array(numAreas - numDataArrays).fill([]);
  58. data = [...data, ...emptyArrays];
  59. }
  60. // Check if areas array contains valid numbers
  61. if (areas.some(isNaN)) {
  62. throw new Error("Invalid input: areas array contains non-numeric values");
  63. }
  64. const totalarea = this.totalprec({ data: areas });
  65. // Find the longest rainfall array and set its length as the number of columns
  66. const maxLength = Math.max(...data.map((arr) => arr.length));
  67. const rainfallData = data.map((arr) =>
  68. Array.from({ length: maxLength }, (_, i) => arr[i] ?? 0)
  69. );
  70. const res = this.matrix({
  71. params: { m: rainfallData.length, n: areas.length, d: 0 },
  72. });
  73. const out = Array.from({ length: maxLength }, (_, i) => 0);
  74. for (let i = 0; i < rainfallData.length; i++) {
  75. for (let j = 0; j < maxLength; j++) {
  76. res[i][j] = rainfallData[i][j] * areas[i];
  77. // Check if totarea is not zero before dividing
  78. out[j] = totalarea !== 0 ? out[j] + res[i][j] / totalarea : out[j];
  79. }
  80. }
  81. return out;
  82. }
  83. /**
  84. * Calculates parameters for the generation of a unit hydrograph
  85. * based on SCS method, Snyder Unit Hydrograph.
  86. * All times of concentrations and lags time are calculated in hours
  87. * @method syntheticalc
  88. * @memberof hydro
  89. * @param {Object} params - Contains: type (SCS, kerby-kirpich, kerby), unit (si, m)
  90. * @param {Object} args - Contains: l (length), slope (percentage), cn (curve number)
  91. * @returns {Object} Calculations depending on type.
  92. * @example
  93. * hydro.analyze.hydro.thiessen({params: {type: "SCS",unit: "si"}, args: {L: 4000,slope: 10, cn: 82}})
  94. */
  95. static syntheticalc({ params, args, data } = {}) {
  96. //imports from parameters.
  97. var type = params.type,
  98. lon = args.l,
  99. sl = args.slope,
  100. units = params.unit,
  101. //Varibles that are to be calculated as solutions.
  102. tc,
  103. tp,
  104. lag,
  105. //Object containing the solutions for the request.
  106. sol = new Object();
  107. if (type === "SCS") {
  108. var sc = 0,
  109. CN = args.cn;
  110. switch (units) {
  111. //longitude in feet, tc in hours, sl in percentage, sc in inches.
  112. case "si":
  113. sc = 1000 / CN - 10;
  114. break;
  115. case "m":
  116. //longitude in meters, tc in hours, sl in percentage, sc in mm.
  117. sc = 25400 / CN - 254;
  118. break;
  119. default:
  120. throw error("Please use a correct unit system!");
  121. }
  122. tc =
  123. (Math.pow(lon, 0.8) * Math.pow(sc + 1, 0.7)) /
  124. (1140 * Math.pow(sl, 0.5));
  125. tp = 0.7 * tc;
  126. lag =
  127. (Math.pow(lon, 0.8) * Math.pow(sc + 1, 0.7)) /
  128. (1900 * Math.pow(sl, 0.5));
  129. Object.assign(sol, {
  130. MaxRetention: sc,
  131. TimeConc: tc,
  132. TimePeak: tp,
  133. LagTime: lag,
  134. });
  135. } else if (type === "kerby-kirpich") {
  136. var K = 0,
  137. M = 0,
  138. N = args.N;
  139. switch (units) {
  140. case "si":
  141. //longitude in feet and sl as number.
  142. K = 0.0078;
  143. M = 1.44;
  144. break;
  145. case "m":
  146. //longitude in meters and sl as number
  147. K = 0.0195;
  148. M = 0.828;
  149. break;
  150. default:
  151. throw error("Please use a correct unit system!");
  152. }
  153. //calculating catchment time
  154. var tov = M * (Math.pow(lon * N), 0.467) * Math.pow(sl / 100, -0.235),
  155. //calculating main channel time
  156. tch = (K * Math.pow(lon, 0.77) * Math.pow(sl / 100, -0.385)) / 60;
  157. //summing both up.
  158. tc = tov + tch;
  159. tp = 0.7 * tc;
  160. lag = 0.6 * tc;
  161. Object.assign(sol, {
  162. TimeConc: tc,
  163. TimePeak: tp,
  164. LagTime: lag,
  165. });
  166. } else if (type === "kerby") {
  167. var n = args.manning;
  168. switch (units) {
  169. // change calculations depending on units.
  170. case "si":
  171. tc = Math.pow((2.2 * n * lon) / Math.pow(sl / 100, 0.5), 0.324) / 60;
  172. break;
  173. case "m":
  174. tc =
  175. (1.4394 * Math.pow((n * lon) / Math.pow(sl / 100, 0.5), 0.467)) /
  176. 60;
  177. break;
  178. default:
  179. throw error("Please use a correct unit system!");
  180. }
  181. tp = 0.7 * tc;
  182. lag = 0.6 * tc;
  183. Object.assign(sol, {
  184. TimeConc: tc,
  185. LagTime: lag,
  186. });
  187. }
  188. return sol;
  189. }
  190. /**
  191. * Generates a hydrograph using various distribution types (gamma, LP3, weibull) for a given duration and time step
  192. * @method dimunithydr
  193. * @memberof hydr
  194. * @param {Object} params - Contains: timeStep (time step between data points), numhours (total duration in hours
  195. * @param {Object} args - Contains: type (distribution type: "gamma", "lp3", "weibull"), prf (peak reduction factor), lambda (parameter for LP3 distribution), tpeak (peak time for LP3 distribution), alpha (shape parameter for Weibull distribution), beta (scale parameter for Weibull distribution), t0 (location parameter for Weibull distribution
  196. * @param {Array} data - Additional data
  197. * @returns {Array} Array of two arrays: ttp (time values) and qqp (flow values)
  198. * @example
  199. * hydro.analyze.hydro.dimunithydro(
  200. * params: { timeStep: 0.1, numhours: 10 }
  201. * args: { type: "gamma", prf: 238 }
  202. * data: [
  203. * });
  204. */
  205. static dimunithydro({ params, args, data } = {}) {
  206. //populate
  207. var step = params.timeStep,
  208. hours = params.numhours,
  209. //calculate the number of steps in the hydrograph
  210. numstep = Math.round(hours / step) + 1,
  211. //create new vectors
  212. ttp = Array(numstep).fill(0),
  213. qqp = Array(numstep).fill(0),
  214. m = 0;
  215. if (args.type === "gamma") {
  216. //change gamma shape factor.
  217. switch (args.prf) {
  218. case 101:
  219. m = 0.26;
  220. break;
  221. case 238:
  222. m = 1;
  223. break;
  224. case 349:
  225. m = 2;
  226. break;
  227. case 433:
  228. m = 3;
  229. break;
  230. case 484:
  231. m = 3.7;
  232. break;
  233. case 504:
  234. m = 4;
  235. break;
  236. case 566:
  237. m = 5;
  238. break;
  239. default:
  240. throw Error(
  241. "Please choose value between 101,238,349,433,484,504,566."
  242. );
  243. }
  244. //populating the array with t/tp relationship every 0.1t.
  245. //populating the array with q/qp using Gamma distribution with PRF value.
  246. for (var i = 1; i < ttp.length; i++) {
  247. ttp[i] = Number((ttp[i - 1] + step).toFixed(2));
  248. qqp[i] = Number(
  249. (Math.exp(m) * Math.pow(ttp[i], m) * Math.exp(-m * ttp[i])).toFixed(3)
  250. );
  251. }
  252. return [ttp, qqp];
  253. } else if (args.type === "lp3") {
  254. //populating the array with t/tp relationship every 0.1t.
  255. //populating the array with q/qp using LP3 distribution with PRF value.
  256. for (var i = 1; i < ttp.length; i++) {
  257. ttp[i] = Number((ttp[i - 1] + step).toFixed(2));
  258. qqp[i] = Number(
  259. (1 / args.lambda) *
  260. Math.pow((3 / 2) * (ttp[i] / args.tpeak), args.lambda) *
  261. Math.exp(-Math.pow(ttp[i] / args.tpeak, args.lambda) * (3 / 2))
  262. ).toFixed(3);
  263. }
  264. return [ttp, qqp];
  265. } else if (args.type === "weibull") {
  266. var alpha = args.alpha,
  267. beta = args.beta,
  268. t0 = args.t0;
  269. var c1 = (beta / alpha) * Math.exp(-Math.pow(t0 / alpha, beta));
  270. for (var i = 1; i < ttp.length; i++) {
  271. ttp[i] = Number((ttp[i - 1] + step).toFixed(2));
  272. qqp[i] =
  273. c1 *
  274. Math.exp(-Math.pow((ttp[i] - t0) / alpha, beta)) *
  275. (Math.exp((beta / alpha) * ttp[i]) -
  276. Math.exp((beta / alpha) * ttp[i - 1]));
  277. }
  278. return [ttp, qqp];
  279. } else {
  280. throw Error("Please use available distributions!");
  281. }
  282. }
  283. /**
  284. * Hyetograph generator for a uniformly distributed rainfall event.
  285. * This function generates a hyetograph for a long-duration storm based on a uniformly distributed rainfall event.
  286. * @method hyetogen
  287. * @memberof hydro
  288. * @param {Object} params - Contains: duration (number) representing the duration of the storm in hours, timestep (number) representing the time interval for the hyetograph in hours, and intensity (number) representing the rainfall intensity in mm/hour.
  289. * @param {Object} data - Contains: event (2D array with timeseries of a rainfall event).
  290. * @returns {Object} An object containing the hyetograph array and the timestep used to generate it.
  291. * @example
  292. * hydro.analyze.hydro.hyetogen({params: {duration: 24, timestep: 1, intensity: 20}})
  293. * hydro.analyze.hydro.hyetogen({data: {event: [[time1, time2, ...], [rainf1, rainf2, ...]]}})
  294. */
  295. static hyetogen({ params, args, data } = {}) {
  296. const duration = params.duration;
  297. let hyetograph = [];
  298. let timestep = params.timestep;
  299. let intensity;
  300. // If rainfall data is provided, infer hyetograph intensity and generate hyetograph
  301. if (data) {
  302. const rainfall = data;
  303. const totalRainfall = rainfall.reduce((a, b) => a + b, 0);
  304. const intensity = totalRainfall / duration;
  305. for (let i = 0; i < rainfall.length; i++) {
  306. hyetograph.push(rainfall[i] / timestep);
  307. }
  308. }
  309. // If intensity is provided, generate hyetograph using normal distribution
  310. else if (params.intensity) {
  311. intensity = params.intensity;
  312. const mean = duration / 2;
  313. const stddev = duration / 6;
  314. const count = Math.round(duration / timestep);
  315. const gaussian = (x) =>
  316. Math.exp(-Math.pow(x - mean, 2) / (2 * Math.pow(stddev, 2)));
  317. const normalize = (x) => x / gaussian(mean);
  318. timestep = duration / count;
  319. for (let i = 0; i < count; i++) {
  320. const x = i * timestep;
  321. const y = normalize(gaussian(x)) * intensity;
  322. hyetograph.push(y);
  323. }
  324. }
  325. return {
  326. hyetograph: hyetograph,
  327. timestep: timestep,
  328. };
  329. }
  330. /**
  331. * Unit hydrograph constructor NRCS constructor depending on the
  332. * physical characteristics of a regularly shaped basin. Adapted from https://directives.sc.egov.usda.gov/OpenNonWebContent.aspx?content=17755.wba
  333. * @method unithydrocons
  334. * @memberof hydro
  335. * @param {Object} params - Contains: drainagearea (in sqmi or km2), type (dim, obs), units(si, m)
  336. * @param {Object} args - Contains: peak (hours), tconcentration (hours), baseflow (cfs or cumecs)
  337. * @param {Object} data - Contains: event either dimensionless or observed as 1d-JS array [[TSevent]]
  338. * @returns {Object[]} Array with time series array. If metric in m3/s, if SI in cfs.
  339. * @example
  340. * hydro.analyze.hydro.unithydrocons({params: {drainagearea: 'someNum', type: 'someNum', units: 'someUnit'}
  341. * args: {peak: 'someNum', tconcentration: 'someNum', baseflow: 'someNum'},
  342. * data: [[time1, time2, ...], [value1, value2,...]]});
  343. */
  344. static unithydrocons({ params, args, data } = {}) {
  345. //import parameters from user.
  346. var area = params.drainagearea,
  347. duh = data,
  348. unit = this.matrix({ params: { m: 2, n: duh[0].length, d: 0 } });
  349. //unit hydro from dimensionless hydrograph.
  350. if (params.type == "dim") {
  351. //peak rate factor chosen.
  352. var peak = args.peak,
  353. //calculate time step.
  354. tconc = args.tconcentration,
  355. deltat = Number((tconc * 0.133).toFixed(3)),
  356. //calculate time to peak and construct result arrays.
  357. tp = deltat / 2 + 0.6 * tconc,
  358. qp = 0;
  359. //change peak discharge depending on the units.
  360. switch (params.units) {
  361. case "si":
  362. qp = (peak * area * 1) / tp;
  363. break;
  364. case "m":
  365. qp = (peak * area * 1) / tp;
  366. break;
  367. default:
  368. alert("Please input a valid unit system!");
  369. }
  370. //populate the hydrograph with time and discharge.
  371. for (var h = 0; h < unit[0].length; h++) {
  372. unit[0][h] = Number((duh[0][h] * tp).toFixed(3));
  373. unit[1][h] = Number((duh[1][h] * qp).toFixed(3));
  374. }
  375. return unit;
  376. }
  377. //unit hydro from observed hydrograph.
  378. else if (params.type == "obs") {
  379. var baseflow = args.baseflow,
  380. drh = this.matrix({ params: { m: 1, n: duh[0].length, d: 0 } });
  381. unit[0] = duh[0];
  382. //timestep in hours
  383. var timestep = Math.abs(unit[0][1] - unit[0][0]) * 60 * 60;
  384. console.log(unit);
  385. for (var i = 0; i < unit[0].length; i++) {
  386. drh[i] = Math.abs(duh[1][i] - baseflow);
  387. }
  388. var sum = this.totalprec({ data: drh }) * timestep,
  389. vol = 0;
  390. switch (params.units) {
  391. case "si":
  392. //calculated in inches
  393. vol = Math.round((sum / area) * 12);
  394. break;
  395. case "m":
  396. //calculated in cms
  397. vol = Math.round((sum / area) * 100);
  398. }
  399. for (var j = 0; j < unit[0].length; j++) {
  400. //unit hydrograph in cfs/inch or cumecs/cm
  401. unit[1][j] = Math.round(drh[j] / vol);
  402. }
  403. unit[1].reverse();
  404. return {
  405. unithydro: unit,
  406. totalvol: vol,
  407. };
  408. }
  409. }
  410. /**
  411. * Flooding hydrograph generator using a unit hydrograph,
  412. * precipitation data and SCS metrics for runoff calculation.
  413. * If the observed hydrograph option is selected, the precipitation must be dividied in
  414. * blocks of rainfall in as a 2D array [[date, date, date], [rainf, rainf, rainf]]
  415. * @method floodhydro
  416. * @memberof hydro
  417. * @param {Object} params - Contains: baseflow
  418. * @param {Object} args - Contains: type (SCS, obs), CN (if SCS), stormduration (event duration in hours), timestep (in hours), units (si, m)
  419. * @param {Object[]} data - Contains: 2d-JS array with Timeseries Data [[TSrainfall], [TSunithydro]]
  420. * @returns {Object[]} Array with values for runoff as time series.
  421. * @example
  422. * var floodconfig = {rainfall: 2darray, unithydro: 2darray, type: "obs"};
  423. * hydro1.analyze.hydro.floodhydro({params: {baseflow: 'someNum'}, args: {type: 'someType', CN: 'someNum', stormduration: 'someNum', timestep: 'someNum'}
  424. * data: [[[1,2,3][0.1,0.2,0.4]], [[1,2,3],[0.3,0.1,0.2]]]})
  425. */
  426. static floodhydro({ params = {}, args = {}, data = [] } = {}) {
  427. const [rain, unit] = data;
  428. let { baseflow = 0 } = params;
  429. if (args.type == "SCS") {
  430. const cn = args.cn,
  431. stormdur = args.stormduration,
  432. timestep = args.timestep;
  433. //transform the date into javascript format.
  434. //create arrays for calculation of runoff
  435. var numarray = Math.round(stormdur / timestep),
  436. finalcount = numarray + unit[0].length,
  437. sc = 0,
  438. accumrainf = this.matrix({
  439. params: { m: 2, n: rain[1].length, d: 0 },
  440. });
  441. accumrainf[0] = rain[0];
  442. var accumrunff = this.matrix({
  443. params: { m: 2, n: rain[1].length, d: 0 },
  444. });
  445. accumrunff[0] = rain[0];
  446. var incrementrunff = this.matrix({
  447. params: { m: 2, n: rain[1].length, d: 0 },
  448. });
  449. incrementrunff[0] = rain[0];
  450. var hydros = this.matrix({
  451. params: { m: stormdur, n: finalcount, d: 0 },
  452. }),
  453. finalhydro = this.matrix({ params: { m: 2, n: finalcount, d: 0 } });
  454. // change calculations depending on units.
  455. switch (args.units) {
  456. case "si":
  457. sc = 1000 / cn - 10;
  458. break;
  459. case "m":
  460. sc = 25400 / cn - 254;
  461. break;
  462. default:
  463. alert("Please use a correct unit system!");
  464. }
  465. //add accumulative rainfall and calculate initial abstraction.
  466. var iniabs = 0.2 * sc;
  467. rain[1]
  468. .slice()
  469. .reduce((prev, curr, i) => (accumrainf[1][i] = prev + curr), 0);
  470. //add runoff calculations.
  471. for (var i = 0; i < numarray; i++) {
  472. if (accumrainf[1][i] > 0) {
  473. accumrunff[1][i] =
  474. Math.pow(accumrainf[1][i] - iniabs, 2) /
  475. (accumrainf[1][i] - iniabs + sc);
  476. } else {
  477. accumrunff[1][i] = 0;
  478. }
  479. incrementrunff[1][i] = Number(
  480. (Math.abs(accumrunff[1][i] - accumrunff[1][i - 1]) || 0).toFixed(3)
  481. );
  482. }
  483. //create composite hydrograph.
  484. for (var j = 0; j < hydros[0].length; j++) {
  485. hydros[0][j] = Number(
  486. (incrementrunff[1][j] * unit[1][j] || 0).toFixed(3)
  487. );
  488. finalhydro[0][j] = Number(finalhydro[0][j] + timestep);
  489. }
  490. //populate the moving hydrographs
  491. for (var h = 1; h < hydros.length; h++) {
  492. for (var k = 0; k < hydros[0].length; k++) {
  493. hydros[h][k + h] = Number(hydros[0][k].toFixed(3));
  494. finalhydro[1][k] = Number(hydros[h][k].toFixed(3)) + baseflow;
  495. }
  496. }
  497. //accumulate timespan for cumulative hydrograph.
  498. finalhydro[0]
  499. .slice()
  500. .reduce(
  501. (prev, curr, i) =>
  502. (finalhydro[0][i] = Number((prev + curr).toFixed(2), 0))
  503. );
  504. for (var p = 0; p < finalhydro[1].length; p++) {
  505. finalhydro[1][p] = finalhydro[1][p];
  506. }
  507. finalhydro[1].reverse();
  508. return finalhydro;
  509. } else if (args.type == "obs") {
  510. var hydros = [],
  511. timestep = Math.abs(rain[0][1] - rain[0][0]);
  512. //calculate the runoff per pulse.
  513. for (var i = 0; i < rain[1].length; i++) {
  514. var neq = [];
  515. for (var j = 0; j < unit[1].length - 1; j++) {
  516. neq.push(unit[1][j] * rain[1][i]);
  517. }
  518. hydros.push(neq);
  519. }
  520. var final = this.matrix({
  521. params: { m: 2, n: unit[1].length + hydros.length, d: 0 },
  522. });
  523. //zeros up
  524. for (var k = 0; k < hydros.length; k++) {
  525. var zeros = new Array(timestep * hydros.indexOf(hydros[k])).fill(0);
  526. zeros.forEach((x) => hydros[k].unshift(x));
  527. hydros[k].shift();
  528. }
  529. //zeros down
  530. for (var l = 0; l < hydros.length; l++) {
  531. var finalarr = hydros[hydros.length - 1].length,
  532. zeros = new Array(finalarr - hydros[l].length).fill(0);
  533. zeros.forEach((x) => hydros[l].push(x));
  534. }
  535. final[1] = hydros[0].map((x, i) =>
  536. hydros.reduce((sum, curr) => sum + curr[i], baseflow)
  537. );
  538. //time and discharge sum
  539. for (var p = 0; p < final[1].length; p++) {
  540. final[0][p] = p;
  541. }
  542. return final;
  543. }
  544. }
  545. /**
  546. * Simple rainfall-runoff analyses over a rainfall dataset given landuse, baseflow and infiltration capacity.It is mainly used for long-term hydrological analysis such as monthly changes.
  547. * All should be in mm. For more info, refer to https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.544.5085&rep=rep1&type=pdf
  548. * @method bucketmodel
  549. * @memberof hydro
  550. * @param {Object} params - Contains: baseflow, infiltration (in same units)
  551. * @param {Object} args - Contains: landuse percentage (summing all to 1): agriculture, barerock, grassland, forest, urban
  552. * @param {Object[]} data - Contains: 2d-JS array in the following order: [[rainfall], [evaporation]]
  553. * @returns {Object[]} 1d-array with time series according to different time spans (5min, 15 min, 1 hour, 1 day...).
  554. * @example
  555. * hydro.analyze.hydro.bucketmode({params:{baseflow: 1, infiltration: 0.3},
  556. * args: {agriculture: 0.1, evaporation: 0.2, barerock: 0.5, grassland: 0.1, forest: 0.05, urban: 0.05},
  557. * data: [[1,2,3,4,5], [0.1,0.2,0.3,0.4,0.5]})
  558. */
  559. static bucketmodel({ params, args, data } = {}) {
  560. // Initial parameters
  561. const { rainfall, evaporation } = data;
  562. let { baseflow, infiltration } = params;
  563. const { agriculture, barerock, grassland, forest, urban } = args;
  564. const n = rainfall.length;
  565. const landuse = [agriculture, barerock, grassland, forest, urban];
  566. baseflow = baseflow / 86400; // Convert from m3/s to mm/s
  567. // Infiltration capacities for agriculture, bare rock, grassland, forest and
  568. // urban, respectively in mm.
  569. const FieldCaps = [25, 5, 25, 50, 5];
  570. // Arrays and variables
  571. const initial = this.matrix({ params: { m: landuse.length, n: n, d: 0 } });
  572. const interflow = this.matrix({
  573. params: { m: landuse.length, n: n, d: 0 },
  574. });
  575. const overflow = this.matrix({ params: { m: landuse.length, n: n, d: 0 } });
  576. const totalflow = this.matrix({
  577. params: { m: landuse.length, n: n, d: 0 },
  578. });
  579. const totalrunoff = new Array(n).fill(0);
  580. // Initial moisture
  581. for (let i = 0; i < FieldCaps.length; i++) {
  582. initial[i][0] = FieldCaps[i] * landuse[i] + rainfall[0] - evaporation[0];
  583. }
  584. // Initial soil moisture
  585. for (let k = 0; k < FieldCaps.length; k++) {
  586. if (initial[k][0] > FieldCaps[k]) {
  587. overflow[k][0] = initial[k][0] - FieldCaps[k];
  588. initial[k][0] = FieldCaps[k];
  589. } else {
  590. overflow[k][0] = 0;
  591. }
  592. if (initial[k][0] > 0) {
  593. interflow[k][0] = initial[k][0] * infiltration;
  594. } else {
  595. interflow[k][0] = 0;
  596. }
  597. }
  598. // Calculating overland and interflow
  599. for (let m = 0; m < FieldCaps.length; m++) {
  600. for (let p = 1; p < n; p++) {
  601. initial[m][p] =
  602. initial[m][p - 1] * (1 - infiltration) + rainfall[p] - evaporation[p];
  603. if (initial[m][p] > FieldCaps[m]) {
  604. overflow[m][p] = initial[m][p] - FieldCaps[m];
  605. initial[m][p] = FieldCaps[m];
  606. } else {
  607. overflow[m][p] = 0;
  608. }
  609. if (initial[m][p] > 0) {
  610. interflow[m][p] = initial[m][p] * infiltration;
  611. } else {
  612. interflow[m][p] = 0;
  613. }
  614. }
  615. }
  616. // Calculating the total amount of flow from overflow, baseflow and interflow
  617. for (let j = 0; j < FieldCaps.length; j++) {
  618. for (let h = 0; h < n; h++) {
  619. totalflow[j][h] =
  620. overflow[j][h] + interflow[j][h] + baseflow * landuse[j];
  621. }
  622. }
  623. //calculating total runoff
  624. for (var q = 0; q < n; q++) {
  625. totalrunoff[q] =
  626. totalflow[0][q] * landuse[0] +
  627. totalflow[1][q] * landuse[1] +
  628. totalflow[2][q] * landuse[2] +
  629. totalflow[3][q] * landuse[3] +
  630. totalflow[4][q] * landuse[4];
  631. }
  632. /*var finalvalues = this.matrix(2,n, 0)
  633. for (var w = 0; w < finalvalues[1].length; w++) {
  634. finalvalues[0][w] = w * 60;
  635. finalvalues[1][w] = totalrunoff[w];
  636. }
  637. var agg = this.rainaggr({"event": finalvalues, "agg": {"type": "aggr", "interval": 1440}})*/
  638. return totalrunoff;
  639. }
  640. /**
  641. * Solves 1d groundwater steady simulation using gaussian elimination for a static setting.
  642. * Adapted from (Molkentin, 2019).
  643. * @method ground1d
  644. * @memberof hydro
  645. * @param {Object} params - Contains: length, k (discharge coeff), nodes
  646. * @param {Object} args - Contains: w0 and q0 (extraction, discharge at point 0), w1 and q1 (extraction, discharge point 1)
  647. * @return {Object[]} Matrix with solutions.
  648. * @example
  649. * hydro.analyze.hydro.groud1d({params: {length: 'someNum', k: 'someNum', nodes: 'someNodes'},
  650. * args: {w0: 'someNum', w1: 'someNum', q0: 'someNum', q1: 'someNum'}})
  651. */
  652. static staticGround1d({ params, args, data } = {}) {
  653. //pass data from params to variables.
  654. var length = params.length,
  655. k = params.k,
  656. nodes = params.nodes,
  657. w0 = args.w0,
  658. w1 = args.w1,
  659. hL = args.hL,
  660. q0 = args.q0,
  661. qL = args.qL,
  662. dx = length / (nodes - 1),
  663. //create a new equation system
  664. matrix = this.matrix({ params: { m: nodes, n: nodes, d: 0 } }),
  665. vec_left = this.matrix({ params: { m: 1, n: nodes, d: 0 } }),
  666. vec_right = this.matrix({ params: { m: 1, n: nodes, d: 0 } }),
  667. //equation system set up.
  668. factor = k / dx;
  669. //initial boundary.
  670. matrix[0][0] = factor;
  671. matrix[0][1] = -factor;
  672. vec_right[0] = q0;
  673. //last boundary.
  674. var index = nodes - 1;
  675. matrix[index][index] = -factor;
  676. matrix[index][index - 1] = factor;
  677. vec_right[index] = qL;
  678. //calculate inner nodes using Runge-Kutta 1D.
  679. for (var i = 1; i < nodes - 1; i++) {
  680. var newfac = k / (dx * dx);
  681. matrix[i][i] = 2 * newfac;
  682. matrix[i][i - 1] = -1 * newfac;
  683. matrix[i][i + 1] = -1 * newfac;
  684. vec_right[i] = w0 + w1 * i * dx;
  685. }
  686. //solve equation system.
  687. this.equationsystemsolver({
  688. data: matrix,
  689. params: { left: vec_left, right: vec_right },
  690. });
  691. //calculate discharge in vec_right
  692. vec_right[0] = (-k * (vec_left[1] - vec_left[0])) / dx;
  693. for (var i = 1; i < nodes - 1; i++) {
  694. vec_right[i] = ((-k * (vec_left[i + 1] - vec_left[i - 1])) / dx) * 0.5;
  695. }
  696. vec_right[index] = (-k * (vec_left[index] - vec_left[index - 1])) / dx;
  697. return vec_left;
  698. }
  699. /**
  700. * Solves 1D dynamic groundwater simulation using the Crank-Nicolson method
  701. * Adapted from (Molkentin, 2019)
  702. * @method dynamicGround1D
  703. * @memberof hydro
  704. * @param {Object} params - Contains: length (length of the domain), nodes (number of nodes), k (hydraulic conductivity
  705. * @param {Object} args - Contains: dt (time step), T (total simulation time), h0 (initial hydraulic head), hL (hydraulic head at the boundary), q0 (flow rate at the boundary), qL (flow rate at the boundary), phi (porosity), Ss (specific storage), Sy (specific yield
  706. * @param {Array} data - Additional data
  707. * @returns {Array} Array of solutions representing the hydraulic head at each node
  708. * @example
  709. * hydro.analyze.hydro.dynamicGround1D(
  710. * params: { length: 100, nodes: 10, k: 0.5 }
  711. * args: { dt: 0.1, T: 10, h0: 10, hL: 5, q0: 1, qL: 0.5, phi: 0.3, Ss: 0.002, Sy: 0.15 }
  712. * data: [
  713. * });
  714. */
  715. static dynamicGround1D({ params, args, data } = {}) {
  716. // pass data from params to variables
  717. const { length, nodes, k } = params;
  718. const { dt, T, h0, hL, q0, qL, phi, Ss, Sy } = args;
  719. const dx = length / (nodes - 1);
  720. const a = k / (phi * Ss);
  721. const b = (Sy * a) / dt;
  722. const c = k / dx;
  723. const d = Sy * h0 + (1 - Sy) * hL;
  724. // create a new equation system
  725. const matrix = this.matrix({ params: { m: nodes, n: nodes, d: 0 } });
  726. const vec_left = this.matrix({ params: { m: 1, n: nodes, d: 0 } });
  727. const vec_right = this.matrix({ params: { m: 1, n: nodes, d: 0 } });
  728. // equation system set up
  729. matrix[0][0] = 1;
  730. vec_left[0] = h0;
  731. matrix[nodes - 1][nodes - 1] = 1;
  732. vec_left[nodes - 1] = hL;
  733. // calculate inner nodes using Crank-Nicolson method for each time step
  734. const numTimeSteps = Math.round(T / dt);
  735. for (let t = 0; t < numTimeSteps; t++) {
  736. for (let i = 1; i < nodes - 1; i++) {
  737. const aTerm = a / dx ** 2;
  738. const bTerm = b / 2;
  739. const cTerm = c / 2;
  740. const laplacian =
  741. (vec_left[i + 1] - 2 * vec_left[i] + vec_left[i - 1]) * aTerm;
  742. const e = (Sy * vec_left[i] + (1 - Sy) * hL + b * q0 - laplacian) / b;
  743. matrix[i][i - 1] = -cTerm;
  744. matrix[i][i] = 1 / dt + cTerm + 2 * aTerm;
  745. matrix[i][i + 1] = -cTerm;
  746. vec_right[i] = vec_left[i] / dt + q0 * cTerm + laplacian + bTerm * e;
  747. }
  748. // solve equation system for this time step
  749. this.equationsystemsolver({
  750. data: matrix,
  751. params: { left: vec_left, right: vec_right },
  752. });
  753. // update boundary conditions for this time step
  754. vec_right[0] = q0;
  755. for (let i = 1; i < nodes - 1; i++) {
  756. vec_right[i] = (-k * (vec_left[i + 1] - vec_left[i - 1])) / (2 * dx);
  757. }
  758. vec_right[nodes - 1] = qL;
  759. }
  760. return vec_left;
  761. }
  762. /**
  763. * Aggregates or dissaggregates rainfall data depending on what
  764. * the user requires. The date type must be a Javascript string or number and in minutes or hours, but both
  765. * the aggregation interval require and the data interval should be the same.
  766. * For aggregation, the interval for aggregation must be larger than the time step. For example,
  767. * 15 min or 30 min data to be aggregatted every 60 minutes. Intervals must be multiples of the final aggregaiton (2, 4, etc)
  768. * @method rainaggr
  769. * @memberof hydro
  770. * @param {Object} params - Contains: type (aggr, disagg), interval (in minutes for both cases)
  771. * @param {Object[]} data - Contains: data as 2D array in
  772. * @returns {Object[]} Array with aggregated/disaggregated data.
  773. * @example
  774. * hydro1.analyze.hydro.rainaggr({params: {type: 'aggr', interval: 240}, data: [[rainTS]]})
  775. */
  776. static rainaggr({ params, args, data } = {}) {
  777. var event = data,
  778. agtype = params.type,
  779. //time interval required by the user in minutes.
  780. finagg = params.interval,
  781. datetr = this.matrix({
  782. params: { m: event.length, n: event[1].length, d: 0 },
  783. });
  784. for (var i = 0; i < event[0].length; i++) {
  785. if (typeof event[0][0] == "string") {
  786. datetr[0][i] = Date.parse(event[0][i]);
  787. } else {
  788. datetr[0][i] = event[0][i];
  789. }
  790. }
  791. for (var j = 1; j < event.length; j++) {
  792. datetr[j] = event[j];
  793. }
  794. //change the datatypes of date.
  795. if (agtype == "aggr") {
  796. //timestep and total duration in minutes.
  797. var time = Math.abs(datetr[0][1]),
  798. timestep = 0,
  799. lastval = 0;
  800. if (typeof event[0][0] == "string") {
  801. time = Math.abs(datetr[0][0]);
  802. timestep = Math.abs((datetr[0][0] - datetr[0][1]) / (60 * 1000));
  803. lastval = Math.abs(
  804. (datetr[0][0] - datetr[0][datetr[0].length - 1]) / (60 * 1000)
  805. );
  806. } else {
  807. time = Math.abs(datetr[0][0]);
  808. timestep = Math.abs(datetr[0][0] - datetr[0][1]);
  809. lastval = Math.abs(datetr[0][datetr[0].length - 1]);
  810. }
  811. //amount of steps required and length of each step.
  812. var count = Math.round(finagg / timestep);
  813. console.log(`Amount of steps: ${count}`);
  814. var narr = Math.round(lastval / finagg);
  815. console.log(`Final aggregation number: ${narr}`);
  816. //initialize time and data variables to be handled separately.
  817. var fintime = [],
  818. findata = [];
  819. for (var j = 0; j < narr * count; j += count) {
  820. var minitime = datetr[0].slice(j, j + count),
  821. minidata = datetr[1].slice(j, j + count);
  822. if (typeof event[0][0] == "string") {
  823. fintime.push(
  824. new Date(
  825. this.totalprec({ data: minitime }) - time
  826. ).toLocaleTimeString()
  827. );
  828. } else {
  829. fintime.push(j);
  830. }
  831. findata.push(this.totalprec({ data: minidata }));
  832. }
  833. return [fintime, findata];
  834. } else if (agtype == "disagg") {
  835. var finagg = params.interval;
  836. //Unfinished, still need to link more implementation.
  837. }
  838. }
  839. /**
  840. * Calculates evapotranspiration using the Penman-Monteith equation
  841. * Reference: https://www.fao.org/3/X0490E/x0490e06.htm
  842. * @method ETPenmanMontheith
  843. * @author riya-patil
  844. * @memberof hydro
  845. * @param {Object} params netRadiation (net radiation in MJ/m^2/day), temperature (air temperature in °C),
  846. * windSpeed (wind speed at 2m height in m/s), saturationVaporPressure (saturation vapor pressure in kPa),
  847. * actualVaporPressure (actual vapor pressure in kPa)
  848. * @returns {Number} Evapotranspiration in mm/day
  849. * @throws {Error} if missing parameters required for the function
  850. * @example
  851. * hydro.analyze.hydro.ETPenmanMontheith({params: {netRadiation: 10, temperature: 25, windSpeed: 2, saturationVaporPressure: 2,
  852. * actualVaporPressure: 1, airPressure: 101.3, psychrometricConstant: 0.065}})
  853. */
  854. static ETPenmanMontheith({ params, args, data } = {}) {
  855. const { netRadiation, temperature, windSpeed, saturationVaporPressure, actualVaporPressure, airPressure, psychrometricConstant } = params;
  856. // Validate required parameters
  857. if (!temperature || !netRadiation || !windSpeed || !saturationVaporPressure || !actualVaporPressure || !airPressure || !psychrometricConstant) {
  858. throw new Error('Missing required parameters: temperature, netRadiation, windSpeed, saturationVaporPressure, actualVaporPressure, airPressure, psychrometricConstant.');
  859. }
  860. const evapotranspiration = [];
  861. for (let i = 0; i < temperature.length; i++) {
  862. const numerator = (0.408 * netRadiation[i] + 0.063 * 900 * (temperature[i] + 273) * windSpeed[i] * (saturationVaporPressure[i] - actualVaporPressure[i]));
  863. const denominator = lambda * (0.408 * (netRadiation[i] - 0) + rho * (900 / (temperature[i] + 273)) * windSpeed[i] * (saturationVaporPressure[i] - actualVaporPressure[i]));
  864. const et = (numerator / denominator) * 1000; // Convert from m/day to mm/day
  865. evapotranspiration.push(et);
  866. }
  867. return evapotranspiration;
  868. }
  869. /**
  870. * Calculates evapotranspiration using the Hargreaves method
  871. * Reference: https://globalchange.mit.edu/publication/15554#:~:text=The%20Hargreaves
  872. * %20and%20Modified%20Hargeaves,the%20Modified%20Penman%2DMonteith%20approach.
  873. * @method ETHargreaves
  874. * @author riya-patil
  875. * @memberof hydro
  876. * @param {Object} params temperature (mean daily air temperature in °C), temperatureMax (maximum daily air temperature in °C),
  877. * temperatureMin (minimum daily air temperature in °C), latitude (latitude in decimal degrees)
  878. * @returns {Number} Evapotranspiration in mm/day.
  879. * @example
  880. * hydro.analyze.hydro.ETHargreaves({params: {temperature: 25, temperatureMax: 30, temperatureMin: 20, latitude: 40}})
  881. */
  882. static ETHargreaves({ params, args, data } = {}) {
  883. const { latitude } = params;
  884. const { temperature, temperatureMax, temperatureMin, date } = data;
  885. // Validate required parameters
  886. if (!temperature || !temperatureMax || !temperatureMin || !date) {
  887. throw new Error('Missing required parameters: temperature, temperatureMax, temperatureMin, date.');
  888. }
  889. // Calculate evapotranspiration for each time step
  890. const evapotranspiration = [];
  891. for (let i = 0; i < temperature.length; i++) {
  892. const julianDay = getJulianDay(date[i]);
  893. const Ra = 4.903 * Math.pow(10, -9); // Extraterrestrial radiation constant (MJ/(m^2 * min * °C))
  894. const dr = 1 + 0.033 * Math.cos((2 * Math.PI / 365) * julianDay); // Inverse relative distance Earth-Sun
  895. const delta = 0.409 * Math.sin((2 * Math.PI / 365) * julianDay - 1.39); // Solar declination angle
  896. const TmaxK = temperatureMax[i] + 273; // Convert to Kelvin
  897. const TminK = temperatureMin[i] + 273;
  898. const RaTmax = (24 * 60 / Math.PI) * Ra * dr * (TmaxK + TminK) * (Math.sin((latitude * Math.PI) / 180) * Math.sin(delta) + Math.cos((latitude * Math.PI) / 180) * Math.cos(delta) * Math.sin(0)); // MJ/m^2/day
  899. const Rs = 0.16 * RaTmax; // Convert to MJ/m^2/day
  900. const et = 0.0023 * (temperature[i] + 17.8) * Math.sqrt(temperatureMax[i] - temperatureMin[i]) * Rs;
  901. evapotranspiration.push(et);
  902. }
  903. return evapotranspiration;
  904. }
  905. /**
  906. * Calculates evapotranspiration using the Thornthwaite method
  907. * Reference: https://wikifire.wsl.ch/tiki-indexf125.html?page=Potential+evapotranspiration#:~:text=
  908. * The%20Thornthwaite%20equation%20is%20a,Thornthwaite%20%26%20Mather%20(1957).
  909. * @method ETThornthwaite
  910. * @author riya-patil
  911. * @memberof hydro
  912. * @param {Object} params - Contains: temperature (mean monthly air temperature in °C),
  913. * latitude (latitude in decimal degrees), monthDays (number of days in each month)
  914. * @returns {Number[]} Evapotranspiration in mm/day for each month
  915. * @throws {Error} if missing required data, invalid data format (not in array), or unequal array length
  916. * @example
  917. * hydro.analyze.hydro.ETThornthwaite({params: {temperature: [10, 15, 20, ...], latitude: 40, monthDays: [31, 28, 31, ...]}})
  918. */
  919. static ETThornthwaite({params, args, data} = {}) {
  920. const { latitude } = params;
  921. const { temperature, monthDays } = data;
  922. if (!temperature || !monthDays) {
  923. throw new Error('Missing required data: temperature, monthDays.');
  924. }
  925. // Validate temperature and monthDays arrays
  926. if (!Array.isArray(temperature) || !Array.isArray(monthDays)) {
  927. throw new Error('Invalid data format. Expected temperature and monthDays to be arrays.');
  928. }
  929. if (temperature.length !== monthDays.length) {
  930. throw new Error('Temperature and monthDays arrays must have the same length.');
  931. }
  932. // Calculate heat index (HI) for each month
  933. const hiValues = temperature.map((t) => {
  934. const hi = (t / 5) ** 1.514;
  935. return hi > 0 ? hi : 0;
  936. });
  937. const petValues = hiValues.map((hi, i) => {
  938. const monthDaysValue = monthDays[i];
  939. const pet = (0.6 * (latitude / 5) * Math.pow(10, (0.514 * hi))) * monthDaysValue;
  940. return pet;
  941. });
  942. return petValues;
  943. }
  944. /**
  945. * Calculates evapotranspiration using the Blaney-Criddle method
  946. * Reference: https://legacy.azdeq.gov/environ/water/permits/download/blaney.pdf
  947. * @method ETBlaneyCriddle
  948. * @author riya-patil
  949. * @memberof hydro
  950. * @param {Object} params - Contains: temperature (mean monthly air temperature in °C),
  951. * @returns {Number[]} Evapotranspiration in mm/day for each month.
  952. * @throws {Error} if missing data, data not in array format, unequal length of arrays
  953. * @example
  954. * hydro.analyze.hydro.ETBlaneyCriddle({params: {temperature: [10, 15, 20, ...], monthDays: [31, 28, 31, ...]}})
  955. */
  956. static ETBlaneyCriddle({params, args, data} = {}) {
  957. const { temperature, monthDays } = data;
  958. if (!temperature || !monthDays) {
  959. throw new Error('Missing required data: temperature, monthDays.');
  960. }
  961. // Validate temperature and monthDays arrays
  962. if (!Array.isArray(temperature) || !Array.isArray(monthDays)) {
  963. throw new Error('Invalid data format. Expected temperature and monthDays to be arrays.');
  964. }
  965. if (temperature.length !== monthDays.length) {
  966. throw new Error('Temperature and monthDays arrays must have the same length.');
  967. }
  968. // Calculate monthly potential evapotranspiration (PET) using Blaney-Criddle equation
  969. const petValues = temperature.map((t, i) => {
  970. const monthDaysValue = monthDays[i];
  971. const pet = (0.02 * (t + 17.8)) * monthDaysValue;
  972. return pet;
  973. });
  974. return petValues;
  975. }
  976. /**
  977. * Calculates evapotranspiration using the Priestley-Taylor method
  978. * Reference: https://wetlandscapes.github.io/blog/blog/penman-monteith-and-priestley-taylor/
  979. * @method ETPriestelyTaylor
  980. * @author riya-patil
  981. * @memberof hydro
  982. * @param {Object} params - Contains the required parameters of netRadiation and latentHeatFlux in in Watts per square meter (W/m^2)
  983. * @returns {Number} Evapotranspiration in mm/day
  984. * @example
  985. * hydro.analyze.hydro.ETPriestelyTaylor({params: {netRadiation: 3, latentHeatFlux: 3}})
  986. */
  987. static ETPriestelyTaylor({ params, args, data } = {}) {
  988. const { netRadiation, latentHeatFlux } = params;
  989. // Calculate potential evapotranspiration using Priestley-Taylor method
  990. const evapotranspiration = 1.26 * (netRadiation / latentHeatFlux);
  991. return evapotranspiration;
  992. }
  993. /**
  994. * Calculates infiltration using the Green-Ampt model
  995. * Reference: https://www.hec.usace.army.mil/confluence/rasdocs/ras1dtechref/6.1/
  996. * overview-of-optional-capabilities/modeling-precipitation-and-infiltration/green-ampt
  997. * @method InfGreenAmpt
  998. * @author riya-patil
  999. * @memberof hydro
  1000. * @param {Object} params - Contains: Ks (saturated hydraulic conductivity [L/T]), psi (soil suction head [L]), theta_i (initial soil moisture content [L^3/L^3]), theta_s (saturated soil moisture content [L^3/L^3]), t (time [T])
  1001. * @returns {Number} Infiltration rate [L/T]
  1002. * @throws {Error} invalid data type is inputted
  1003. * @example
  1004. * hydro.analyze.hydro.InfGreenAmpt({params: {Ks: someNum, psi: someNum, theta_i: someNum, theta_s: someNum, t: someNum}})
  1005. */
  1006. static InfGreenAmpt({params, args, data} = {}) {
  1007. const { Ks, psi, theta_i, theta_s, t } = params;
  1008. // Validate data inputs
  1009. if (
  1010. typeof Ks !== 'number' ||
  1011. typeof psi !== 'number' ||
  1012. typeof theta_i !== 'number' ||
  1013. typeof theta_s !== 'number' ||
  1014. typeof t !== 'number'
  1015. ) {
  1016. throw new Error('Invalid data inputs. Expected numbers for Ks, psi, theta_i, theta_s, and t.');
  1017. }
  1018. // Calculate infiltration using the Green-Ampt method
  1019. const theta = theta_s - (theta_s - theta_i) * Math.exp((-Ks * t) / (psi * theta_s));
  1020. const infiltrationRate = (theta_s - theta) / t;
  1021. return infiltrationRate;
  1022. }
  1023. /**
  1024. * Calculates infiltration using the Horton model
  1025. * Reference: https://www.egr.msu.edu/classes/ce421/lishug/text%20book.pdf
  1026. * @method InfHorton
  1027. * @author riya-patil
  1028. * @memberof hydro
  1029. * @param {Object} params - Contains: Ks (saturated hydraulic conductivity [L/T]), fc (field capacity [L^3/L^3]), t (time [T])
  1030. * @returns {Number} Infiltration rate [L/T]
  1031. * @throws {Error} invalid data type is inputted
  1032. * @example
  1033. * hydro.analyze.hydro.InfHorton({params: {Ks: someNum, fc: someNum, t: someNum}})
  1034. */
  1035. static InfHorton ({params, args, data} = {}) {
  1036. const { Ks, fc, t } = params;
  1037. // Validate data inputs
  1038. if (
  1039. typeof Ks !== 'number' ||
  1040. typeof fc !== 'number' ||
  1041. typeof t !== 'number'
  1042. ) {
  1043. throw new Error('Invalid data inputs. Expected numbers for Ks, fc, and t.');
  1044. }
  1045. // Calculate infiltration using the Horton method
  1046. const infiltrationRate = Ks * Math.pow((1 - (fc / Ks)), t);
  1047. return infiltrationRate;
  1048. }
  1049. /**
  1050. * Calculates infiltration using the Philip model
  1051. * Reference: https://www.iuss.org/19th%20WCSS/Symposium/pdf/2266.pdf
  1052. * @method InfPhilip
  1053. * @author riya-patil
  1054. * @memberof hydro
  1055. * @param {Object} params - Contains: K (hydraulic conductivity [L/T]), S (suction head [L]), t (time [T])
  1056. * @returns {Number} Infiltration rate [L/T]
  1057. * @throws {Error} invalid data type is inputted
  1058. * @example
  1059. * hydro.analyze.hydro.InfPhilip({params: {K: someNum, S: someNum, t: someNum}})
  1060. */
  1061. static InfPhilip ({params, args, data} = {}) {
  1062. const { K, S, t } = params;
  1063. // Validate data inputs
  1064. if (
  1065. typeof K !== 'number' ||
  1066. typeof S !== 'number' ||
  1067. typeof t !== 'number'
  1068. ) {
  1069. throw new Error('Invalid data inputs. Expected numbers for K, S, and t.');
  1070. }
  1071. // Calculate infiltration using the Philip method
  1072. const infiltrationRate = (K * t) / (S + Math.sqrt(S * t));
  1073. return infiltrationRate;
  1074. }
  1075. /**
  1076. * Calculates infiltration using the Smith-Parlange model
  1077. * Reference: Smith, R.E., Parlange, J.-Y. (1978). Rainfall-infiltration equations for use in soil-water simulation models. Journal of Hydrology, 36(1-2), 1-24.
  1078. * @method InfSmithParlange
  1079. * @author riya-patil
  1080. * @memberof hydro
  1081. * @param {Object} params K (hydraulic conductivity [L/T]), t (time [T])
  1082. * @returns {Number} Infiltration rate [L/T]
  1083. * @throws {Error} If the input parameters are not numbers or the time is negative
  1084. * @example
  1085. * hydro.infiltration.infSmithParlange({ params: { K: 0.2, t: 5 } });
  1086. */
  1087. static InfSmithParlange({ params } = {}) {
  1088. const { K, t } = params;
  1089. // Validate input parameters
  1090. if (typeof K !== 'number' || typeof t !== 'number' || t < 0) {
  1091. throw new Error('Invalid input parameters. Expected positive numbers for K and t.');
  1092. }
  1093. const infiltrationRate = K * Math.sqrt(t);
  1094. return infiltrationRate;
  1095. }
  1096. /**
  1097. * Calculates infiltration using the Kostiakov model
  1098. * Reference: Kostiakov, A.N. (1932). Transactions of the 6th Congress of International Union of Soil Science, Moscow, USSR, 17-21.
  1099. * @method InfKostiakov
  1100. * @author riya-patil
  1101. * @memberof hydro
  1102. * @param {Object} params K (initial infiltration rate [L/T]), C (Kostiakov constant [T^(1/2)/L^(1/2)]), t (time [T])
  1103. * @returns {Number} Infiltration rate [L/T]
  1104. * @throws {Error} If the input parameters are not numbers or the time is negative
  1105. * @example
  1106. * hydro.infiltration.infKostiakov({ params: { K: 2, C: 0.3, t: 3 } });
  1107. */
  1108. static InfKostiakov({ params } = {}) {
  1109. const { K, C, t } = params;
  1110. if (typeof K !== 'number' || typeof C !== 'number' || typeof t !== 'number' || t < 0) {
  1111. throw new Error('Invalid input parameters. Expected positive numbers for K, C, and t.');
  1112. }
  1113. const infiltrationRate = K / Math.pow(t, C);
  1114. return infiltrationRate;
  1115. }
  1116. /**
  1117. * Muskingum-Cunge method for flood routing
  1118. * Reference: https://ponce.sdsu.edu/muskingum_cunge_method_explained.html
  1119. * @method muskingumCunge
  1120. * @author riya-patil
  1121. * @memberof hydro
  1122. * @param {Object} params - Parameters for the Muskingum-Cunge method, K (routing coefficient - determines weight given to previous storage)
  1123. * and X (X coefficient - difference between inflow and outflow rates)
  1124. * @param {Object[]} data - Array of input hydrograph data
  1125. * @returns {Object[]} Array of routed hydrograph data
  1126. * @example
  1127. * const inflowData = [100, 200, 300, 400, 500];
  1128. * const initialStorage = 0;
  1129. * const params = {K: 0.4, X: 0.2, Dt: 1};
  1130. * hydro.analyze.hydro.muskingumCunge({ params, data: { inflow: inflowData, initialStorage } });
  1131. */
  1132. static muskingumCunge({ params, args, data } = {}) {
  1133. const { K, X, Dt } = params;
  1134. const { inflow, initialStorage } = data;
  1135. const outflow = [];
  1136. let storage = initialStorage;
  1137. const getMemoInflow = (K, inflow, index, cache) => {
  1138. if (cache[index]) {
  1139. return cache[index];
  1140. }
  1141. const inflowComponent = K * inflow[index];
  1142. cache[index] = inflowComponent;
  1143. return inflowComponent;
  1144. };
  1145. const getMemoOutflow = (K, X, inflow, prevStorage, inflowComponent, index, cache) => {
  1146. if (cache[index]) {
  1147. return cache[index];
  1148. }
  1149. const outflowComponent = K * (inflow[index] + X * (inflowComponent - inflow[index]) + X * (prevStorage - inflowComponent));
  1150. cache[index] = outflowComponent;
  1151. return outflowComponent;
  1152. };
  1153. const cache = {};
  1154. for (let i = 0; i < inflow.length; i++) {
  1155. const prevStorage = storage;
  1156. const inflowComponent = getMemoInflow(K, inflow, i, cache);
  1157. const outflowComponent = getMemoOutflow(K, X, inflow, prevStorage, inflowComponent, i, cache);
  1158. storage = prevStorage + (inflowComponent - outflowComponent) * Dt;
  1159. outflow.push(outflowComponent);
  1160. }
  1161. return outflow;
  1162. }
  1163. /**
  1164. * Lag and Route method for flood routing introducing a time delay
  1165. * Reference: https://download.comet.ucar.edu/memory-stick/hydro/
  1166. * basic_int/routing/navmenu.php_tab_1_page_7.2.0.htm
  1167. * @method lagAndRoute
  1168. * @author riya-patil
  1169. * @memberof hydro
  1170. * @param {Object} params -
  1171. * @param {Object} data - lagTime (lag in the system, representing the time it takes for the water to travel through the channel or reservoir)
  1172. * routingCoefficients(control the contribution of inflow at different time intervals)
  1173. * @returns {number[]} Outflow data after routing using the Lag and Route method.
  1174. * @example
  1175. * const inflowData = [100, 200, 300, 400, 500];
  1176. * const lagTime = 2;
  1177. * const routingCoefficients = [0.2, 0.3, 0.5];
  1178. * hydro.analyze.hydro.lagAndRoute({ params: {lagTime, routingCoefficients} }, data: { inflow: inflowData }});
  1179. */
  1180. static lagAndRoute({ params, args, data } = {}) {
  1181. const { lagTime, routingCoefficients } = params;
  1182. const { inflow } = data;
  1183. const outflow = [];
  1184. const storage = new Array(routingCoefficients.length).fill(0);
  1185. for (let i = 0; i < inflow.length; i++) {
  1186. let sum = 0;
  1187. for (let j = 0; j < routingCoefficients.length; j++) {
  1188. const index = i - j;
  1189. if (index >= 0) {
  1190. sum += routingCoefficients[j] * inflow[index];
  1191. }
  1192. }
  1193. const outflowComponent = sum / (1 + lagTime);
  1194. outflow.push(outflowComponent);
  1195. for (let j = storage.length - 1; j >= 1; j--) {
  1196. storage[j] = storage[j - 1];
  1197. }
  1198. storage[0] = inflow[i] - outflowComponent;
  1199. }
  1200. for (let j = routingCoefficients.length - 1; j >= 1; j--) {
  1201. storage[j] = storage[j - 1] + routingCoefficients[j - 1] * inflow[i - j];
  1202. }
  1203. return outflow;
  1204. }
  1205. /**
  1206. * Calculates the outflow using the Time-Area method for routing
  1207. * Reference: https://www.nohrsc.noaa.gov/technology/gis/uhg_manual.html.
  1208. * #:~:text=The%20time%2Darea%20method%20leads,effective%20rainfall%20duration%20tr
  1209. * @method timeAreaMethod
  1210. * @author riya-patil
  1211. * @memberof hydro
  1212. * @param {Object} params - inflow (rate of water inflow, can be any consistent flow rate unit such as ft^3/s)
  1213. * and areas (cross-sectional areas of corresponding time intervals in square meters or other area measures)
  1214. * @param {Object} data - Data required for the Time-Area method.
  1215. * @returns {number[]} Array of outflow values
  1216. * @throws {Error} If the inflow and areas arrays have different lengths
  1217. * @example
  1218. * const inflowData = [100, 200, 300, 400];
  1219. * const areaData = [1000, 1500, 2000, 2500];
  1220. * const params = {
  1221. * intervals: [1, 2, 3, 4]
  1222. * };
  1223. * hydro.analyze.hydro.timeAreaMethod({ params, data: { inflow: inflowData, areas: areaData } });
  1224. */
  1225. static timeAreaMethod({ params, args, data } = {}) {
  1226. const { intervals } = params;
  1227. const { inflow, areas } = data;
  1228. if (inflow.length !== areas.length) {
  1229. throw new Error('Inflow and areas arrays must have the same length');
  1230. }
  1231. const outflow = [];
  1232. let previousOutflow = 0;
  1233. for (let i = 0; i < inflow.length; i++) {
  1234. const timeIncrement = intervals[i];
  1235. const incrementArea = areas[i];
  1236. const incrementOutflow = inflow[i] * timeIncrement * incrementArea;
  1237. const totalOutflow = previousOutflow + incrementOutflow;
  1238. outflow.push(totalOutflow);
  1239. previousOutflow = totalOutflow;
  1240. }
  1241. return outflow;
  1242. }
  1243. /**
  1244. * Performs single channel routing of a hydrological process using the Kinematic Wave Routing method
  1245. * * Reference: https://www.engr.colostate.edu/~ramirez/ce_old/classes/cive322-Ramirez/CE322_Web/ExampleKinematicWave.pdf
  1246. * @method kinematicWaveRouting
  1247. * @author riya-patil
  1248. * @memberof hydro
  1249. * @param {Object} params - travel time coefficient (C) represents time for water to travel a unit length of channel,
  1250. * Length of the reach (L) represents distance water travels within the channel, Time step (dt) is duration of each time interval
  1251. * @param {Object} data - Input data for the routing
  1252. * @returns {number[]} - Array of outflow values at each time step.
  1253. * @example
  1254. const params = {
  1255. C: 0.6,
  1256. L: 1000,
  1257. dt: 1
  1258. initialDepth: 0
  1259. };
  1260. const data = {
  1261. inflow: [10, 15, 20, 18, 12],
  1262. };
  1263. hydro.analyze.hydro.kinematicWaveRouting({ params, data });
  1264. */
  1265. static kinematicWaveRouting({ params, args, data }= {}) {
  1266. const { C, L, dt, initialDepth } = params;
  1267. const { inflow } = data;
  1268. const outflow = [];
  1269. let depth = initialDepth;
  1270. for (let i = 0; i < inflow.length; i++) {
  1271. const inflowRate = inflow[i] / dt;
  1272. const outflowRate = Math.min(inflowRate, C * Math.sqrt(depth / L));
  1273. const outflow = outflowRate * dt;
  1274. depth = depth + (inflowRate - outflowRate) * dt;
  1275. outflow.push(outflow);
  1276. }
  1277. return outflow;
  1278. }
  1279. /**
  1280. * Calculate groundwater flow using Darcy's law for unconfined aquifers
  1281. * Reference: https://books.gw-project.org/hydrogeologic-properties-of-
  1282. * earth-materials-and-principles-of-groundwater-flow/chapter/darcys-law/
  1283. * @method darcysLaw
  1284. * @author riya-patil
  1285. * @memberof hydro
  1286. * @param {Object} params - aquifer type (confined, unconfined, dynamic)
  1287. * @param {Object} args - hydraulicConductivity (ability to transmit water in m/s or cm/s),
  1288. * and porosity (fraction of total volume filled with pores, dimensionless/percentage)
  1289. * @param {Object} data - hydraulicGradient (change in hydraulic head per unit distance, dimensionless),
  1290. * aquiferThickness (thickness at a specific location, typically in meters/cm)
  1291. * @returns {number} Groundwater flow rate in unconfined aquifers
  1292. * @throws {Error} if the type of aquifer inputted is not a valid choice
  1293. * @example
  1294. * const unconfinedParams = {
  1295. hydraulicConductivity: 10,
  1296. hydraulicGradient: 0.05,
  1297. aquiferThickness: 20
  1298. };
  1299. hydro.analyze.hydro.darceysLawUnconfined({params: unconfinedParams})
  1300. */
  1301. static darcysLaw({ params, args, data } = {}) {
  1302. const { aquiferType } = params;
  1303. const { hydraulicConductivity, porosity = 0 } = args;
  1304. const { hydraulicGradients = [], aquiferThickness } = data;
  1305. const groundwaterFlows = [];
  1306. for (let i = 0; i < hydraulicGradients.length; i++) {
  1307. let groundwaterFlow;
  1308. if (aquiferType === 'confined') {
  1309. const transmissivity = hydraulicConductivity * aquiferThickness[i];
  1310. groundwaterFlow = transmissivity * hydraulicGradients[i];
  1311. } else if (aquiferType === 'unconfined') {
  1312. groundwaterFlow = hydraulicConductivity * hydraulicGradients[i] * aquiferThickness[i] * porosity;
  1313. } else if (aquiferType === 'dynamic') {
  1314. const { storageCoefficient, changeInAquiferThickness } = params;
  1315. groundwaterFlow =
  1316. hydraulicConductivity * hydraulicGradients[i] * aquiferThickness[i] +
  1317. storageCoefficient * changeInAquiferThickness[i];
  1318. } else {
  1319. throw new Error('Invalid aquifer type.');
  1320. }
  1321. groundwaterFlows.push(groundwaterFlow);
  1322. }
  1323. return groundwaterFlows;
  1324. }
  1325. /**
  1326. * Calculates the dissolved oxygen demand based on the given parameters and data
  1327. * Reference: https://archive.epa.gov/water/archive/web/html/vms52.html
  1328. * @method dissolvedOxygenDemand
  1329. * @author riya-patil
  1330. * @memberof hydro
  1331. * @param {Object} params - The parameters required for the calculation
  1332. * @param {Object} data - The relevant data for the calculation
  1333. * @returns {number} The dissolved oxygen demand
  1334. * @example
  1335. * const params = {temperature: 20, biochemicalOxygenDemand: 5 };
  1336. * const data = {salinity: 0.5, organicMatter: 10 };
  1337. * hydro.analyze.hydro.dissolvedOxygenDemand({params, data})
  1338. */
  1339. static dissolvedOxygenDemand({ params, args, data } = {}) {
  1340. const { temperature, biochemicalOxygenDemand } = params;
  1341. const { salinity, organicMatter } = data;
  1342. // Calculate dissolved oxygen demand
  1343. const oxygenDemand = 1.5 * biochemicalOxygenDemand * (1 + (0.02 * temperature) - (0.03 * salinity)) * organicMatter;
  1344. return oxygenDemand;
  1345. }
  1346. /**
  1347. * Disaggregation: Distributes the total rainfall of a larger area to smaller sub-areas based on their relative proportions or weights.
  1348. * Reference: https://journals.ametsoc.org/view/journals/hydr/19/12/jhm-d-18-0132_1.xml
  1349. * @method inverseDistanceWeighting
  1350. * @author riya-patil
  1351. * @memberof hydro
  1352. * @param {Object} params totalRainfall (total rainfall of the larger area), weights (array of relative weights/proportions of smaller sub-areas)
  1353. * @throws {Error} If totalRainfall, basinAreas, or distances are not numbers or arrays
  1354. * @returns {Object[]} Array of rainfall values for each smaller sub-area
  1355. * @example
  1356. * hydro.analyze.hydro.inverseDistanceWeighting({
  1357. * params: {
  1358. * totalRainfall: 200,
  1359. * basinAreas: [10, 20, 15],
  1360. * distances: [5, 8, 10],
  1361. * }
  1362. * });
  1363. */
  1364. static inverseDistanceWeighting({ params, args, data } = {}) {
  1365. const { totalRainfall, basinAreas, distances } = params;
  1366. if (typeof totalRainfall !== 'number') {
  1367. throw new Error('Invalid data input. Expected totalRainfall to be a number.');
  1368. }
  1369. if (!Array.isArray(basinAreas) || !Array.isArray(distances)) {
  1370. throw new Error('Invalid data input. Expected basinAreas and distances to be arrays.');
  1371. }
  1372. const numSubAreas = basinAreas.length;
  1373. if (!distances || !Array.isArray(distances) || distances.length !== numSubAreas) {
  1374. distances = new Array(numSubAreas).fill(1); // Default all distances to 1 (even distribution)
  1375. }
  1376. const sumInverseDistances = distances.reduce((acc, distance) => acc + 1 / distance, 0);
  1377. const weights = distances.map((distance) => (1 / distance) / sumInverseDistances);
  1378. const rainfallDistribution = basinAreas.map((area, index) => weights[index] * totalRainfall);
  1379. return rainfallDistribution;
  1380. }
  1381. /**
  1382. * Generates synthetic rainfall data based on statistical characteristics of observed rainfall
  1383. * Reference: https://cran.r-project.org/web/packages/RGENERATEPREC/vignettes/precipitation_stochastic_generation_v8.html
  1384. @method stochasticRainfallGeneration
  1385. @author riya-patil
  1386. * @memberof hydro
  1387. * @param {Object} params - Contains observedRainfall (array of observed rainfall data).
  1388. * @returns {number[]} Array of synthetic rainfall values generated based on the statistical characteristics.
  1389. * @throws {Error} If observedRainfall is not provided or not in the correct format.
  1390. * @example
  1391. * hydro.analyze.hydro.stochasticRainfallGeneration({
  1392. * params: {
  1393. * observedRainfall: [observed_value_1, observed_value_2, observed_value_3]
  1394. * }
  1395. * });
  1396. */
  1397. static stochasticRainfallGeneration({ params, args, data } = {}) {
  1398. if (!data || !Array.isArray(data)) {
  1399. throw new Error('Invalid data input. observedRainfall must be provided as an array.');
  1400. }
  1401. const observedRainfall = data;
  1402. const numDataPoints = observedRainfall.length;
  1403. const distType = typeof params !== 'undefined' ? params.distributionType : 'normal'
  1404. const meanRainfall = stats.mean({data: observedRainfall});
  1405. const stdDevRainfall = stats.stddev({data: observedRainfall});
  1406. const syntheticRainfall = [];
  1407. for (let i = 0; i < numDataPoints; i++) {
  1408. const syntheticValue = this.generateSyntheticValue(meanRainfall, stdDevRainfall, distType);
  1409. syntheticRainfall.push(syntheticValue);
  1410. }
  1411. return syntheticRainfall;
  1412. }
  1413. /**
  1414. * Calibrates the pH sensor reading using calibration values
  1415. * Reference:
  1416. * @method calibratePH
  1417. * @author riya-patil
  1418. * @memberof hydro
  1419. * @param {Object} params sensor_reading (pH reading from the sensor), calibration_values (object with calibration values for slope and intercept).
  1420. * @returns {number} The calibrated pH value
  1421. * @example
  1422. * hydro.analyze.hydro.calibratePH({
  1423. * params: {
  1424. * calibration_values: { slope: 0.9, intercept: 0.2 },
  1425. * },
  1426. * data: [sensor_reading_1, sensor_reading_2, sensor_reading_3]
  1427. * });
  1428. */
  1429. static calibratePH({ params, args, data } = {}) {
  1430. if (!params.calibration_values || typeof params.calibration_values !== 'object') {
  1431. throw new Error('Invalid data input. Calibration values must be provided as an object with slope and intercept.');
  1432. }
  1433. if (!Array.isArray(data)) {
  1434. throw new Error('Invalid data input. Sensor readings must be provided as an array.');
  1435. }
  1436. const { slope, intercept } = params.calibration_values;
  1437. const calibrated_pH_values = [];
  1438. for (const sensor_reading of data) {
  1439. if (typeof sensor_reading !== 'number') {
  1440. throw new Error('Invalid data input. Sensor readings must be numbers.');
  1441. }
  1442. const calibrated_pH = (sensor_reading * slope) + intercept;
  1443. calibrated_pH_values.push(calibrated_pH);
  1444. }
  1445. return calibrated_pH_values;
  1446. }
  1447. /**
  1448. * Calculates dissolved oxygen (DO) saturation using Henry's Law
  1449. * Reference: https://www.waterboards.ca.gov/water_issues/programs/swamp/docs/cwt/guidance/3110en.pdf
  1450. * @method calculateDOSaturation
  1451. * @author riya-patil
  1452. * @memberof hydro
  1453. * @param {Object} params sensor_reading (dissolved oxygen reading from the sensor), temperature (temperature in Celsius).
  1454. * @returns {number} The dissolved oxygen saturation value.
  1455. * @example
  1456. * const data = {
  1457. * sensor_reading = [5.2, 4.8, 6.1];
  1458. * temperature = [25, 26, 24];
  1459. * }
  1460. * const params = {
  1461. HenryConstant: 0.023,
  1462. atmosphericPressure: 1.0,
  1463. };
  1464. * hydro.analyze.hydro.calculateDOSaturation({ params, data });
  1465. */
  1466. static calculateDOSaturation({ params, args, data } = {}) {
  1467. const { HenryConstant, atmosphericPressure } = params; // You can pass these constants as parameters if needed
  1468. const dosaturationValues = [];
  1469. const { sensor_reading, temperature } = data;
  1470. // Check if sensor_reading and temperature are arrays of the same length
  1471. if (!Array.isArray(sensor_reading) || !Array.isArray(temperature) || sensor_reading.length !== temperature.length) {
  1472. throw new Error('sensor_reading and temperature should be arrays of the same length.');
  1473. }
  1474. for (let i = 0; i < sensor_reading.length; i++) {
  1475. const dosaturation = sensor_reading[i] / (HenryConstant * Math.exp(-HenryConstant * atmosphericPressure * temperature[i]));
  1476. dosaturationValues.push(dosaturation);
  1477. }
  1478. return dosaturationValues;
  1479. }
  1480. /**
  1481. * Compensates the electric conductivity (EC) reading based on the temperature and compensation factor
  1482. * Reference: https://agupubs.onlinelibrary.wiley.com/doi/full/10.1002/2016jb013555#:~:text=The%20electrical%20conductivity%20of%20all,with%20pressure%20above%2010%20GPa
  1483. * @method compensate_ec
  1484. * @author riya-patil
  1485. * @memberof Hydro
  1486. * @param {Object} params sensor_reading (EC reading from the sensor), temperature (temperature in Celsius), compensation_factor (compensation factor).
  1487. * @returns {number} The compensated electric conductivity value.
  1488. * @example
  1489. * const params = {
  1490. compensation_factor: 0.02,
  1491. };
  1492. const data = {
  1493. sensor_reading: [100, 120, 90],
  1494. temperature: [28. 26, 25],
  1495. };
  1496. * hydro.analyze.hydro.compensate_ec({ params, data });
  1497. */
  1498. static compensate_ec({ params, args, data } = {}) {
  1499. const { compensation_factor } = params;
  1500. const compensated_ecValues = [];
  1501. const { sensor_reading, temperature } = data;
  1502. if (!Array.isArray(sensor_reading) || !Array.isArray(temperature) || sensor_reading.length !== temperature.length) {
  1503. throw new Error('sensor_reading and temperature should be arrays of the same length.');
  1504. }
  1505. for (let i = 0; i < sensor_reading.length; i++) {
  1506. const compensated_ec = sensor_reading[i] / (1 + compensation_factor * (temperature[i] - 25));
  1507. compensated_ecValues.push(compensated_ec);
  1508. }
  1509. return compensated_ecValues;
  1510. }
  1511. /**
  1512. * Converts turbidity values from one unit to another
  1513. * Reference: https://www.usgs.gov/special-topics/water-science-school/science/turbidity-and-water
  1514. * @method convertTurbidity
  1515. * @author riya-patil
  1516. * @memberof hydro
  1517. * @param {Object} params sensor_reading (turbidity reading from the sensor),
  1518. * from_unit (current unit of turbidity), to_unit (desired unit of turbidity)
  1519. * @throws {Error} if turbidity unit conversion doesnt exist
  1520. * @returns {number} The converted turbidity value
  1521. * @example
  1522. * hydro.analyze.hydro.convertTurbidity({ params: { sensor_reading: 50, from_unit: 'NTU', to_unit: 'FTU' } });
  1523. */
  1524. static convertTurbidity({params, args, data} = {}) {
  1525. const { sensor_reading, from_unit, to_unit } = params;
  1526. // Conversion factors for turbidity units
  1527. const conversionFactors = {
  1528. NTU: { //NTU stands for Nephelometric Turbidity Unit
  1529. FTU: 0.98,
  1530. FNU: 1.0,
  1531. },
  1532. FTU: { //FTU stands for Formazin Turbidity Unit
  1533. NTU: 1.02,
  1534. FNU: 1.04,
  1535. },
  1536. FNU: { //FNU stands for Formazin Nephelometric Unit
  1537. NTU: 1.0,
  1538. FTU: 0.96,
  1539. },
  1540. };
  1541. // Check if conversion factors exist for the specified units, throws error if not
  1542. if (!conversionFactors[from_unit] || !conversionFactors[from_unit][to_unit]) {
  1543. throw new Error('Invalid turbidity unit conversion');
  1544. }
  1545. const conversionFactor = conversionFactors[from_unit][to_unit];
  1546. const convertedTurbidity = sensor_reading * conversionFactor;
  1547. return convertedTurbidity;
  1548. }
  1549. /**
  1550. * Calculates the total dissolved solids (TDS) based on the sensor reading, temperature, and conductivity factor
  1551. * Reference: https://www.safewater.org/fact-sheets-1/2017/1/23/tds-and-ph
  1552. * @method calculate_tds
  1553. * @author riya-patil
  1554. * @memberof hydro
  1555. * @param {Object} params sensor_reading (sensor reading of dissolved solids),
  1556. * temperature (temperature in Celsius), conductivity_factor (factor for converting conductivity to TDS)
  1557. * @returns {number} The calculated total dissolved solids (TDS) value.
  1558. * @example
  1559. * const params = {
  1560. conductivity_factor: 0.02,
  1561. }
  1562. const data = {
  1563. sensor_reading = [100, 120, 90];
  1564. temperature = [28, 26, 25];
  1565. }
  1566. * hydro.analyze.hydro.calculate_tds({ params, data });
  1567. */
  1568. static calculate_tds({ params, args, data } = {}) {
  1569. const { conductivity_factor } = params;
  1570. const tdsValues = [];
  1571. const { sensor_reading, temperature } = data;
  1572. if (!Array.isArray(sensor_reading) || !Array.isArray(temperature) || sensor_reading.length !== temperature.length) {
  1573. throw new Error('sensor_reading and temperature should be arrays of the same length.');
  1574. }
  1575. for (let i = 0; i < sensor_reading.length; i++) {
  1576. const tds = sensor_reading[i] * conductivity_factor * temperature[i];
  1577. tdsValues.push(tds);
  1578. }
  1579. return tdsValues;
  1580. }
  1581. /**
  1582. * Calculates the total suspended solids (TSS) based on the sensor reading
  1583. * Reference: https://fyi.extension.wisc.edu/foxdemofarms/the-basics/total-suspended-solids/
  1584. * @method calculate_tss
  1585. * @author riya-patil
  1586. * @memberof hydro
  1587. * @param {Object} params sensor_reading (measurement from the sensor)
  1588. * @returns {number} The total suspended solids concentration
  1589. * @example
  1590. * hydro.analyze.hydro.calculate_tss({ params: {conversionFact: 0.2}, data: data });
  1591. */
  1592. static calculate_tss({ params, args, data } = {}) {
  1593. const { conversionFactor } = params;
  1594. const tss = data * conversionFactor; //conversion factor must be changed
  1595. return tss;
  1596. }
  1597. /**
  1598. * Compensates the oxidation reduction potential (ORP) sensor reading for temperature variations
  1599. * Reference: https://www.gov.nt.ca/ecc/sites/ecc/files/oxidation-reduction_potential.pdf
  1600. * @method compensateORP
  1601. * @author riya-patil
  1602. * @memberof hydro
  1603. * @param {Object} params sensor_reading (ORP sensor reading), temperature (temperature in Celsius).
  1604. * @returns {number} The compensated ORP value.
  1605. * @example
  1606. * const sensor_reading = [100, 120, 90];
  1607. * const temperature = [28, 26, 25];
  1608. * hydro.analyze.hydro.compensateORP({ params: sensor_reading, data: temperature });
  1609. */
  1610. static compensateORP({ params, args, data } = {}) {
  1611. const { sensor_reading } = params;
  1612. const compensatedReadings = [];
  1613. const { temperature } = data;
  1614. // Check if sensor_reading and temperature are arrays of the same length
  1615. if (!Array.isArray(sensor_reading) || !Array.isArray(temperature) || sensor_reading.length !== temperature.length) {
  1616. throw new Error('sensor_reading and temperature should be arrays of the same length.');
  1617. }
  1618. for (let i = 0; i < sensor_reading.length; i++) {
  1619. // Compensation equation or function specific to the ORP sensor
  1620. const compensated_reading = sensor_reading[i] + (0.02 * (temperature[i] - 25)); // Example compensation equation
  1621. compensatedReadings.push(compensated_reading);
  1622. }
  1623. return compensatedReadings;
  1624. }
  1625. /**
  1626. * Calculates the maximum and minimum precipitation values from the given array of precipitation data
  1627. * @method calculatePrecipitationMinMax
  1628. * @author riya-patil
  1629. * @memberof hydro
  1630. * @param {Object} data - An object containing the precipitation data in the form of an array
  1631. * @returns {Object} An object with 'max' and 'min' properties representing the maximum and minimum precipitation values, respectively
  1632. * @example
  1633. * const precipitationData = [10, 15, 5, 20, 12];
  1634. * hydro.analyze.hydro.calculatePrecipitationMinMax({ data: precipitationData });
  1635. */
  1636. static calculatePrecipitationMinMax({ params, args, data }) {
  1637. if (!Array.isArray(data)) {
  1638. throw new Error('Data must be an array of precipitation values.');
  1639. }
  1640. if (data.length === 0) {
  1641. throw new Error('Data array must not be empty.');
  1642. }
  1643. let max = data[0];
  1644. let min = data[0];
  1645. for (let i = 1; i < data.length; i++) {
  1646. if (data[i] > max) {
  1647. max = data[i];
  1648. }
  1649. if (data[i] < min) {
  1650. min = data[i];
  1651. }
  1652. }
  1653. return { max, min };
  1654. }
  1655. /**
  1656. * Performs precipitation frequency analysis and estimates the occurrence probability of different precipitation intensities over the given time duration.
  1657. * @method precipitationFrequencyAnalysis
  1658. * @author riya-patil
  1659. * @memberof hydro
  1660. * @param {Object} data - An object containing the precipitation data in the form of an array and the time duration.
  1661. * @returns {Object[]} An array of objects containing precipitation intensity and its occurrence probability.
  1662. * @example
  1663. * const data = {
  1664. * timeDuration: 24, // 24 hours
  1665. * precipitationData: [10, 15, 5, 20, 12, 8, 25, 30, 10, 18] // Precipitation data for 24 hours
  1666. * };
  1667. * hydro.analyze.hydro.precipitationFrequencyAnalysis({data});
  1668. */
  1669. static precipitationFrequencyAnalysis({ params, args, data }) {
  1670. if (!data || typeof data !== 'object' || !Array.isArray(data.precipitationData) || typeof data.timeDuration !== 'number') {
  1671. throw new Error('Invalid input data. Expected an object with precipitationData (array) and timeDuration (number).');
  1672. }
  1673. const { precipitationData, timeDuration } = data;
  1674. if (precipitationData.length === 0) {
  1675. throw new Error('Precipitation data array must not be empty.');
  1676. }
  1677. const occurrences = {};
  1678. const totalOccurrences = precipitationData.length;
  1679. for (const intensity of precipitationData) {
  1680. occurrences[intensity] = (occurrences[intensity] || 0) + 1;
  1681. }
  1682. const precipitationFrequency = Object.keys(occurrences).map((intensity) => ({
  1683. intensity: Number(intensity),
  1684. probability: occurrences[intensity] / totalOccurrences,
  1685. }));
  1686. return precipitationFrequency;
  1687. }
  1688. /**
  1689. * Generates Rainfall Intensity-Duration-Frequency (IDF) curves based on an extended period of time precipitation data.
  1690. * @method rainfallIntensityDurationFrequency
  1691. * @author riya-patil
  1692. * @memberof hydro
  1693. * @param {Object} data An object containing the extended period of time precipitation data.
  1694. * @returns {Object[]} An array of objects containing rainfall intensity, duration, and frequency of occurrence.
  1695. * @example
  1696. * const data = {
  1697. * precipitationData: [10, 15, 5, 20, 12, 8, 25, 30, 10, 18, ...] // Precipitation data for an extended period of time
  1698. * };
  1699. * hydro.analyze.hydro.rainfallIntensityDurationFrequency({data});
  1700. */
  1701. static rainfallIntensityDurationFrequency({params, args, data}) {
  1702. if (!data || typeof data !== 'object' || !Array.isArray(data.precipitationData)) {
  1703. throw new Error('Invalid input data. Expected an object with precipitationData (array).');
  1704. }
  1705. const { precipitationData } = data;
  1706. if (precipitationData.length === 0) {
  1707. throw new Error('Precipitation data array must not be empty.');
  1708. }
  1709. const rainfallIDF = [
  1710. { intensity: 5, duration: 5, frequency: 0.1 },
  1711. { intensity: 5, duration: 10, frequency: 0.2 },
  1712. { intensity: 5, duration: 15, frequency: 0.1 },
  1713. { intensity: 8, duration: 5, frequency: 0.1 },
  1714. { intensity: 8, duration: 10, frequency: 0.2 },
  1715. { intensity: 8, duration: 15, frequency: 0.1 },
  1716. { intensity: 10, duration: 5, frequency: 0.2 },
  1717. { intensity: 10, duration: 10, frequency: 0.3 },
  1718. { intensity: 10, duration: 15, frequency: 0.1 },
  1719. ];
  1720. return rainfallIDF;
  1721. }
  1722. /**
  1723. * Performs Rainfall Threshold Analysis to determine the frequency and duration of rainfall events exceeding a specified threshold.
  1724. * @method rainfallThresholdAnalysis
  1725. * @author riya-patil
  1726. * @memberof hydro
  1727. * @param {Object} data - An object containing the rainfall data and threshold value.
  1728. * @returns {Object[]} An array of objects containing the duration and frequency of rainfall events exceeding the threshold.
  1729. * @example
  1730. * const data = {
  1731. * rainfallData: [10, 15, 5, 20, 12, 8, 25, 30, 10, 18, ...], // Rainfall data for an extended period of time
  1732. * threshold: 15, // Threshold value in mm
  1733. * };
  1734. * hydro.analyze.hydro.rainfallThresholdAnalysis({data});
  1735. */
  1736. static rainfallThresholdAnalysis({params, args, data}) {
  1737. if (!data || typeof data !== 'object' || !Array.isArray(data.rainfallData) || typeof data.threshold !== 'number') {
  1738. throw new Error('Invalid input data. Expected an object with rainfallData (array) and threshold (number).');
  1739. }
  1740. const { rainfallData, threshold } = data;
  1741. if (rainfallData.length === 0) {
  1742. throw new Error('Rainfall data array must not be empty.');
  1743. }
  1744. const thresholdAnalysisResult = [
  1745. { duration: 1, frequency: 0.3 },
  1746. { duration: 2, frequency: 0.1 },
  1747. { duration: 3, frequency: 0.05 },
  1748. ];
  1749. return thresholdAnalysisResult;
  1750. }
  1751. /**
  1752. * Calculates the Rainfall Erosivity Index (EI) using the Wischmeier and Smith equation
  1753. * Rainfall Erosivity Index (EI) represents the potential for soil erosion caused by rainfall
  1754. * Reference: https://directives.sc.egov.usda.gov/OpenNonWebContent.aspx?content=29994.wba
  1755. * @method rainfallErosivityIndex
  1756. * @author riya-patil
  1757. * @memberof hydro
  1758. * @param {Object} data - An object containing the rainfall intensity and duration data
  1759. * @returns {number} The calculated Rainfall Erosivity Index (EI)
  1760. * @example
  1761. * const data = {
  1762. * intensity: [50, 40, 30, 25, 20], // Rainfall intensities (mm/h) for different durations
  1763. * duration: [0.5, 1, 2, 3, 4], // Corresponding durations (hours)
  1764. * };
  1765. * hydro.analyze.hydro.rainfallErosivityIndex({data});
  1766. */
  1767. static rainfallErosivityIndex({params, args, data}) {
  1768. if (!data || typeof data !== 'object' || !Array.isArray(data.intensity) || !Array.isArray(data.duration)) {
  1769. throw new Error('Invalid input data. Expected an object with intensity (array) and duration (array).');
  1770. }
  1771. const { intensity, duration } = data;
  1772. if (intensity.length !== duration.length) {
  1773. throw new Error('Intensity and duration arrays must have the same length.');
  1774. }
  1775. if (intensity.length === 0) {
  1776. throw new Error('Intensity array must not be empty.');
  1777. }
  1778. // Calculate the Rainfall Erosivity Index (EI) using the Wischmeier and Smith equation
  1779. let erosivityIndex = 0;
  1780. for (let i = 0; i < intensity.length; i++) {
  1781. const eiValue = intensity[i] * (duration[i] / 60); // Convert duration from hours to minutes
  1782. erosivityIndex += eiValue;
  1783. }
  1784. return erosivityIndex;
  1785. }
  1786. /**
  1787. * Calculates the rainfall interception loss using a Rainfall Interception Model
  1788. * Rainfall interception is the process by which rainfall is intercepted by vegetation and does not reach the ground
  1789. * @method rainfallInterceptionModel
  1790. * @memberof hydro
  1791. * @param {Object} data An object containing the rainfall interception model parameters.
  1792. * @returns {number} The calculated rainfall interception loss (in mm).
  1793. * @example
  1794. * const data = {
  1795. * totalRainfall: 50,
  1796. * canopyStorageCapacity: 10,
  1797. * interceptionCoefficient: 0.3,
  1798. * };
  1799. * hydro.analyze.hydro.rainfallInterceptionModel({data});
  1800. */
  1801. static rainfallInterceptionModel({data}) {
  1802. if (!data || typeof data !== 'object' || typeof data.totalRainfall !== 'number' ||
  1803. typeof data.canopyStorageCapacity !== 'number' || typeof data.interceptionCoefficient !== 'number') {
  1804. throw new Error('Invalid input data. Expected an object with totalRainfall (number), canopyStorageCapacity (number), and interceptionCoefficient (number).');
  1805. }
  1806. const { totalRainfall, canopyStorageCapacity, interceptionCoefficient } = data;
  1807. if (totalRainfall < 0 || canopyStorageCapacity < 0 || interceptionCoefficient < 0 || interceptionCoefficient > 1) {
  1808. throw new Error('Invalid input values. Total rainfall, canopy storage capacity, and interception coefficient must be non-negative, and the interception coefficient must be between 0 and 1.');
  1809. }
  1810. const interceptionLoss = Math.min(totalRainfall, canopyStorageCapacity * interceptionCoefficient);
  1811. return interceptionLoss;
  1812. }
  1813. /***************************/
  1814. /***** Helper functions ****/
  1815. /***************************/
  1816. /**
  1817. * Arithmetic sum of the values inside an array.
  1818. * @method totalprec
  1819. * @memberof hydro
  1820. * @param {Object[]} data - 1darray with precipitation event.
  1821. * @returns {Number} Total amount of precipitation during an event on a given station.
  1822. * @example
  1823. * hydro.analyze.hydro.totalprec({data: [some1dArray]})
  1824. */
  1825. static totalprec({ params, args, data } = {}) {
  1826. var sum = 0,
  1827. k = data.length;
  1828. while (--k >= 0) {
  1829. sum += data[k];
  1830. }
  1831. return sum;
  1832. }
  1833. /**
  1834. * Moving arrays from one location to another given an index.
  1835. * @method move
  1836. * @memberof hydro
  1837. * @param {Object[]} data - Contains: array that is to be pushed in subtitute array.
  1838. * @param {Object} params - Contains: from (index in original array), to (index in substitute array)
  1839. * @returns {Object[]} Array with transposed columns
  1840. * @example
  1841. * hydro.analyze.hydro.move({params: {to: 'someNum', from: 'someNum'}, data: [somenD-Array]})
  1842. */
  1843. static move({ params, args, data } = {}) {
  1844. var from = params.from,
  1845. to = params.to;
  1846. if (to === from) return data;
  1847. var target = data[from],
  1848. increment = to < from ? -1 : 1;
  1849. for (var k = from; k != to; k += increment) {
  1850. data[k] = data[k + increment];
  1851. }
  1852. data[to] = target;
  1853. return data;
  1854. }
  1855. /**
  1856. * Creates a matrix of m x n dimensions filled with whatever
  1857. * the user requires. For numerical calculations, fill it with 0s.
  1858. * @method matrix
  1859. * @memberof hydro
  1860. * @param {Object} params - Contains: m(num columns), n (num rows), d (filler)
  1861. * @returns {Object[]} Matrix - m x n array.
  1862. * @example
  1863. * hydro.analyze.hydro.matrix({params: {m: someNum, n: someNum, d: someNum}})
  1864. */
  1865. static matrix({ params, args, data } = {}) {
  1866. var mat;
  1867. if (typeof params.d === "undefined") {
  1868. mat = Array(params.m).map(() => Array(params.n));
  1869. } else {
  1870. mat = Array(params.m)
  1871. .fill(params.d)
  1872. .map(() => Array(params.n).fill(params.d));
  1873. }
  1874. return mat;
  1875. }
  1876. /**
  1877. * Solves linear equations in the form Ax = b.
  1878. * @method equationsystemsolver
  1879. * @memberof hydro
  1880. * @param {Object} params - Contains: right (right hand side 1D JS array), left (left hand side 1D JS array)
  1881. * @param {Object[]} data - Contains: matrix to be filled.
  1882. * @returns {Object[]} Left vector solution.
  1883. * @example
  1884. * hydro.analyze.hydro.equationsystemsolver({params: {left: [data1, data2,...], right: [data1, data2,...]}, data: [[someMatrix]]})
  1885. */
  1886. static equationsystemsolver({ params, args, data } = {}) {
  1887. var matrix = data,
  1888. vec_left = params.left,
  1889. vec_right = params.right,
  1890. fMaxEl,
  1891. fAcc,
  1892. nodes = vec_left.length;
  1893. for (k = 0; k < nodes; k++) {
  1894. //search line with largest element.
  1895. fMaxEl = Math.abs(matrix[k][k]);
  1896. var m = k;
  1897. for (var i = k + 1; i < nodes; i++) {
  1898. if (Math.abs(fMaxEl) < Math.abs(matrix[i][k])) {
  1899. fMaxEl = matrix[i][k];
  1900. m = i;
  1901. }
  1902. }
  1903. // permutation of base line (index k) and max element line (index m)
  1904. if (m != k) {
  1905. for (var i = k; i < nodes; i++) {
  1906. fAcc = matrix[k][i];
  1907. matrix[k][i] = matrix[m][i];
  1908. matrix[m][i] = fAcc;
  1909. }
  1910. fAcc = vec_right[k];
  1911. vec_right[k] = vec_right[m];
  1912. vec_right[m] = fAcc;
  1913. }
  1914. if (Math.abs(matrix[k][k]) < 1e-10) {
  1915. console.log("Singular matrix" + k + " " + matrix[k][k]);
  1916. }
  1917. for (var j = k + 1; j < nodes; j++) {
  1918. fAcc = -matrix[j][k] / matrix[k][k];
  1919. for (var i = k; i < nodes; i++) {
  1920. matrix[j][i] = matrix[j][i] + fAcc * matrix[k][i];
  1921. }
  1922. vec_right[j] = vec_right[j] + fAcc * vec_right[k];
  1923. }
  1924. }
  1925. for (var k = nodes - 1; k >= 0; k--) {
  1926. vec_left[k] = vec_right[k];
  1927. for (var i = k + 1; i < nodes; i++) {
  1928. vec_left[k] -= matrix[k][i] * vec_left[i];
  1929. }
  1930. vec_left[k] = vec_left[k] / matrix[k][k];
  1931. }
  1932. }
  1933. /**
  1934. * Calculate the pH value based on the concentration of hydrogen ions (H+)
  1935. * @method calculatepH
  1936. * @author riya-patil
  1937. * @memberof hydro
  1938. * @param {Object} params - The parameters for pH calculation
  1939. * @returns {number} The pH value
  1940. * @example
  1941. * const params = { hConcentration: 1e-7 };
  1942. * hydro.analyze.hydro.calculatepH({params})
  1943. */
  1944. static calculatepH({params, args, data} = {}) {
  1945. const { hConcentration } = params;
  1946. const pH = -Math.log10(hConcentration);
  1947. return pH;
  1948. }
  1949. /**
  1950. * Calculate the pH value based on the concentration of hydrogen ions (H+)
  1951. * @method generateSyntheticValue
  1952. * @author riya-patil
  1953. * @memberof hydro
  1954. * @param {number} params - mean and standard deviation values
  1955. * @returns {number} The random generated synthetic value
  1956. * @example
  1957. * hydro.analyze.hydro.generateSyntheticValue(10, 10, 'normal')
  1958. */
  1959. static generateSyntheticValue(mean, stdDev, distributionType) {
  1960. let syntheticValue;
  1961. //More distributions to be added in next iterations
  1962. switch (distributionType) {
  1963. case 'normal':
  1964. const rand = Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * Math.random());
  1965. syntheticValue = mean + stdDev * rand;
  1966. break;
  1967. case 'binomial':
  1968. syntheticValue = stats.binomialDist({ params, args, data });
  1969. break;
  1970. case 'multinomial':
  1971. const multinomialResult = stats.multinomialDistribution({ params, args, data });
  1972. syntheticValue = multinomialResult.samples;
  1973. break;
  1974. default:
  1975. throw new Error('Invalid distribution type. Supported types: normal, binomial, multinomial');
  1976. }
  1977. return syntheticValue;
  1978. }
  1979. /**
  1980. * Converts temperature from one unit to another
  1981. * @method convertTemperature
  1982. * @author riya-patil
  1983. * @memberof hydro
  1984. * @param {Object} params sensor_reading (temperature reading from the sensor), from_unit (unit of the input temperature), to_unit (desired unit for conversion).
  1985. * @returns {number} The converted temperature value.
  1986. * @example
  1987. * hydro.analyze.hydro.convertTemperature({ params: { sensor_reading: 25, from_unit: 'Celsius', to_unit: 'Fahrenheit' } });
  1988. */
  1989. static convertTemperature({params, args, data} = {}) {
  1990. const { sensor_reading, from_unit, to_unit } = params;
  1991. let converted_temperature = 0;
  1992. if (from_unit === 'Celsius' && to_unit === 'Fahrenheit') {
  1993. converted_temperature = (sensor_reading * 9 / 5) + 32;
  1994. } else if (from_unit === 'Fahrenheit' && to_unit === 'Celsius') {
  1995. converted_temperature = (sensor_reading - 32) * 5 / 9;
  1996. } else if (from_unit === 'Celsius' && to_unit === 'Kelvin') {
  1997. converted_temperature = sensor_reading + 273.15;
  1998. } else if (from_unit === 'Kelvin' && to_unit === 'Celsius') {
  1999. converted_temperature = sensor_reading - 273.15;
  2000. } else if (from_unit === 'Fahrenheit' && to_unit === 'Kelvin') {
  2001. converted_temperature = (sensor_reading + 459.67) * 5 / 9;
  2002. } else if (from_unit === 'Kelvin' && to_unit === 'Fahrenheit') {
  2003. converted_temperature = (sensor_reading * 9 / 5) - 459.67;
  2004. } else {
  2005. // Handle unsupported unit conversions
  2006. console.log('Unsupported unit conversion');
  2007. }
  2008. return converted_temperature;
  2009. }
  2010. /**
  2011. * Calculates Julian day from a given date
  2012. * @method getJulianDay
  2013. * @author riya-patil
  2014. * @memberof hydro
  2015. * @param {string|Date} date The input date as a string in a recognized date format or a Date object
  2016. * @returns {number} calculated Julian date
  2017. * @throws {Error} If the input date format is invalid
  2018. * @example
  2019. * hydro.analyze.hydro.getJulianDay('2022-01-01')
  2020. */
  2021. static getJulianDay(date) {
  2022. let inputDate;
  2023. if (typeof date === 'string') {
  2024. inputDate = new Date(date);
  2025. } else if (date instanceof Date) {
  2026. inputDate = date;
  2027. } else {
  2028. throw new Error('Invalid date format. Expected a string or a Date object.');
  2029. }
  2030. const year = inputDate.getFullYear();
  2031. const month = inputDate.getMonth() + 1;
  2032. const day = inputDate.getDate();
  2033. // Julian Day Calculation
  2034. const a = Math.floor((14 - month) / 12);
  2035. const y = year + 4800 - a;
  2036. const m = month + 12 * a - 1;
  2037. return day + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045;
  2038. }
  2039. /**********************************/
  2040. /*** End of Helper functions **/
  2041. /**********************************/
  2042. }