apmtools 0.2.15__tar.gz → 0.2.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: apmtools
3
- Version: 0.2.015
3
+ Version: 0.2.017
4
4
  Summary: A collection of tools for processing air pollution monitoring data
5
5
  Project-URL: Homepage, https://github.com/federlorenz/apmtools
6
6
  Author-email: Federico Lorenzetti <lorenzetti.federico@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "apmtools"
7
- version = "0.2.015"
7
+ version = "0.2.017"
8
8
  authors = [
9
9
  { name="Federico Lorenzetti", email="lorenzetti.federico@gmail.com" },
10
10
  ]
@@ -9,12 +9,14 @@ import pandas as pd
9
9
 
10
10
 
11
11
  PLOT_TYPES = {
12
- "line": "Line",
13
12
  "bar": "Bar",
14
- "histogram": "Histogram",
15
13
  "box": "Boxplot",
14
+ "histogram": "Histogram",
15
+ "line": "Line",
16
+ "scatter": "Scatter",
16
17
  }
17
18
 
19
+
18
20
  # ``None`` means row-level plotting. The other values are passed directly to
19
21
  # Summary.group_stat(frequency=...).
20
22
  FREQUENCIES = {
@@ -28,13 +30,13 @@ FREQUENCIES = {
28
30
  }
29
31
 
30
32
  AGGREGATES = {
33
+ "Count": "count",
34
+ "Maximum": "max",
31
35
  "Mean": "mean",
32
36
  "Median": "median",
33
- "Sum": "sum",
34
37
  "Minimum": "min",
35
- "Maximum": "max",
36
- "Count": "count",
37
38
  "Standard deviation": "std",
39
+ "Sum": "sum",
38
40
  }
39
41
 
40
42
  def _default_x(data: pd.DataFrame) -> str:
@@ -55,26 +57,15 @@ def _default_group(data: pd.DataFrame) -> str | None:
55
57
 
56
58
  def _numeric_columns(data: pd.DataFrame) -> list[str]:
57
59
  """Return numeric column names as strings."""
58
- return [
60
+ return sorted([
59
61
  str(column)
60
62
  for column in data.columns
61
63
  if pd.api.types.is_numeric_dtype(data[column])
62
- ]
63
-
64
- def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> list[str]:
65
- """Validate and normalise an initial Y selection."""
66
- numeric = _numeric_columns(data)
67
- if y is None:
68
- return [column for column in numeric if column != x][:1]
69
- values = [y] if isinstance(y, str) else list(y)
70
- missing = [column for column in values if column not in data.columns]
71
- if missing:
72
- raise ValueError(f"Unknown y column(s): {missing}")
73
- return [str(column) for column in values]
64
+ ])
74
65
 
75
66
 
76
67
  def _categorical_columns(data: pd.DataFrame) -> list[str]:
77
- """Return columns suitable for a boxplot categorical X axis."""
68
+ """Return categorical column names as strings."""
78
69
  return sorted(
79
70
  [
80
71
  str(column)
@@ -86,6 +77,41 @@ def _categorical_columns(data: pd.DataFrame) -> list[str]:
86
77
  )
87
78
 
88
79
 
80
+ def _datetime_columns(data: pd.DataFrame) -> list[str]:
81
+ """Return datetime column names as strings."""
82
+ return sorted(
83
+ [
84
+ str(column)
85
+ for column in data.columns
86
+ if pd.api.types.is_datetime64_any_dtype(data[column])
87
+ ],
88
+ key=str.casefold,
89
+ )
90
+
91
+
92
+ def _timedelta_columns(data: pd.DataFrame) -> list[str]:
93
+ """Return timedelta column names as strings."""
94
+ return sorted(
95
+ [
96
+ str(column)
97
+ for column in data.columns
98
+ if pd.api.types.is_timedelta64_dtype(data[column])
99
+ ],
100
+ key=str.casefold,
101
+ )
102
+
103
+ def _default_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> list[str]:
104
+ """Validate and normalise an initial Y selection."""
105
+ numeric = _numeric_columns(data)
106
+ if y is None:
107
+ return [column for column in numeric if column != x][:1]
108
+ values = [y] if isinstance(y, str) else list(y)
109
+ missing = [column for column in values if column not in data.columns]
110
+ if missing:
111
+ raise ValueError(f"Unknown y column(s): {missing}")
112
+ return [str(column) for column in values]
113
+
114
+
89
115
  def _alphabetical(values) -> list[str]:
90
116
  """Return string values sorted alphabetically, case-insensitively."""
91
117
  return sorted((str(value) for value in values), key=str.casefold)
@@ -274,13 +300,16 @@ def create_summary_app(
274
300
  columns = _alphabetical(summary.columns)
275
301
  column_map = {str(c): c for c in summary.columns}
276
302
  numeric_columns = _alphabetical(_numeric_columns(summary))
277
- categorical_columns = _categorical_columns(summary)
303
+ categorical_columns = _alphabetical(_categorical_columns(summary))
304
+ datetime_columns = _alphabetical(_datetime_columns(summary))
305
+ timedelta_columns = _alphabetical(_timedelta_columns(summary))
306
+
278
307
 
279
308
  x = _default_x(summary) if x is None else str(x)
280
309
  if x not in column_map:
281
310
  raise ValueError(f"Unknown x column: {x!r}")
282
311
 
283
- initial_y = _normalise_y(summary, x, y)
312
+ initial_y = _default_y(summary, x, y)
284
313
  if group_by is None:
285
314
  initial_groups = [_default_group(summary)] if _default_group(
286
315
  summary) is not None else []
@@ -340,8 +369,11 @@ def create_summary_app(
340
369
  "datetime_end", "End datetime",
341
370
  value="", placeholder="YYYY-MM-DD or YYYY-MM-DD HH:MM:SS",
342
371
  ),
343
- ui.input_numeric("bins", "Histogram bins",
344
- value=30, min=1, max=200, step=1),
372
+ ui.panel_conditional(
373
+ "input.plot_type === 'histogram'",
374
+ ui.input_numeric("bins", "Histogram bins",
375
+ value=30, min=1, max=200, step=1),
376
+ ),
345
377
  ui.input_text("title", "Plot title", value=title),
346
378
  ui.help_text(
347
379
  "Subset and datetime filters are applied before aggregation. "
@@ -358,39 +390,53 @@ def create_summary_app(
358
390
  def server(input, output, session):
359
391
  @render.ui
360
392
  def x_control():
361
- """Use categorical X choices for boxplots; all columns otherwise."""
362
- if input.plot_type() == "box":
363
- if not categorical_columns:
364
- return ui.help_text(
365
- "Boxplots require at least one categorical column for the X axis."
366
- )
367
- selected = x if x in categorical_columns else categorical_columns[0]
368
- return ui.input_select(
369
- "x", "X-axis / category",
370
- choices={c: c for c in categorical_columns},
371
- selected=selected,
393
+ kind = input.plot_type()
394
+ if kind in ("box", "bar"):
395
+ choices = categorical_columns
396
+ multiple = True
397
+ elif kind == "line":
398
+ choices = datetime_columns
399
+ multiple = False
400
+ else:
401
+ choices = numeric_columns
402
+ multiple = False
403
+
404
+ if not choices:
405
+ return ui.help_text(f"No compatible X variables for {kind}.")
406
+ selected = [x] if x in choices else [choices[0]]
407
+ if multiple:
408
+ return ui.input_selectize(
409
+ "x", "X-axis / categories",
410
+ choices={c: c for c in choices},
411
+ selected=selected, multiple=True,
372
412
  )
373
413
  return ui.input_select(
374
- "x", "X-axis / category",
375
- choices={c: c for c in columns},
376
- selected=x,
414
+ "x", "X-axis / variable",
415
+ choices={c: c for c in choices},
416
+ selected=selected[0],
377
417
  )
378
418
 
379
419
  @render.ui
380
420
  def y_control():
381
- """Boxplots accept one numeric Y variable; other plots accept many."""
382
- if input.plot_type() == "box":
383
- selected = initial_y[0] if initial_y else (
384
- numeric_columns[0] if numeric_columns else None)
421
+ kind = input.plot_type()
422
+ if kind == "histogram":
423
+ return ui.div()
424
+ if not numeric_columns:
425
+ return ui.help_text("No numeric Y variables available.")
426
+ selected = next(
427
+ (v for v in initial_y if v in numeric_columns), numeric_columns[0])
428
+ if kind in ("box", "bar", "scatter"):
385
429
  return ui.input_select(
386
430
  "y", "Y-axis / variable",
387
431
  choices={c: c for c in numeric_columns},
388
432
  selected=selected,
389
433
  )
390
434
  return ui.input_selectize(
391
- "y", "Y-axis / variable",
435
+ "y", "Y-axis / variables",
392
436
  choices={c: c for c in numeric_columns},
393
- selected=initial_y, multiple=True,
437
+ selected=[v for v in initial_y if v in numeric_columns] or [
438
+ selected],
439
+ multiple=True,
394
440
  )
395
441
 
396
442
  @render.ui
@@ -450,89 +496,92 @@ def create_summary_app(
450
496
  @reactive.calc
451
497
  def selected_data():
452
498
  current = filtered_summary()
453
- y_input = input.y()
454
- selected_y = ([y_input] if isinstance(
455
- y_input, str) else list(y_input or []))
456
- if not selected_y:
499
+ kind = input.plot_type()
500
+ x_input = input.x()
501
+ selected_x = ([x_input] if isinstance(
502
+ x_input, str) else list(x_input or []))
503
+ if not selected_x:
504
+ return pd.DataFrame()
505
+
506
+ if kind == "histogram":
507
+ value_names = selected_x
508
+ else:
509
+ y_input = input.y()
510
+ value_names = ([y_input] if isinstance(
511
+ y_input, str) else list(y_input or []))
512
+ if not value_names:
457
513
  return pd.DataFrame()
458
514
 
459
- y_columns = [column_map[c] for c in selected_y]
460
515
  selected_groups = list(input.group_by())
461
516
  grouping_by = [column_map[c] for c in selected_groups]
462
517
  frequency = FREQUENCIES[input.frequency()]
463
518
  aggregate = AGGREGATES[input.aggregate()]
464
-
465
- # A boxplot's categorical X must survive group_stat(). Include it
466
- # as a statistical grouping key when frequency aggregation is used,
467
- # without turning it into a colour/group selection in the UI.
468
- if input.plot_type() == "box":
469
- selected_x = input.x()
470
- if selected_x in column_map:
471
- box_x = column_map[selected_x]
472
- if box_x not in grouping_by:
473
- grouping_by = [*grouping_by, box_x]
474
-
475
- # All selected grouping columns are passed directly to
476
- # Summary.group_stat() when a frequency is selected.
519
+ value_columns = [column_map[c] for c in value_names]
520
+
521
+ if kind in ("box", "bar"):
522
+ for c in selected_x:
523
+ if column_map[c] not in grouping_by:
524
+ grouping_by.append(column_map[c])
525
+ if kind == "scatter" and frequency is not None:
526
+ # Both numerical axes must be aggregated on the same rows.
527
+ value_columns = list(dict.fromkeys(
528
+ [column_map[selected_x[0]], *value_columns]
529
+ ))
477
530
  if frequency is not None:
478
- current = _aggregate(current, y_columns,
479
- frequency, grouping_by, aggregate)
480
-
531
+ current = _aggregate(
532
+ current, value_columns, frequency, grouping_by, aggregate
533
+ )
481
534
  return current
482
535
 
483
536
  @render_plotly
484
537
  def plot():
485
538
  plot_df = selected_data()
486
539
  selected_plot = input.plot_type()
487
- selected_x = input.x()
488
- y_input = input.y()
540
+ x_input = input.x()
541
+ selected_x = ([x_input] if isinstance(
542
+ x_input, str) else list(x_input or []))
543
+ y_input = None if selected_plot == "histogram" else input.y()
489
544
  selected_y = ([y_input] if isinstance(
490
545
  y_input, str) else list(y_input or []))
491
546
  selected_groups = list(input.group_by())
492
547
  frequency = FREQUENCIES[input.frequency()]
493
548
 
494
- if plot_df.empty or not selected_y:
549
+ if plot_df.empty or not selected_x or (
550
+ selected_plot != "histogram" and not selected_y
551
+ ):
495
552
  return px.scatter(title="No data for the selected filters")
496
553
 
497
- # Keep the original selected Y columns separate from the columns
498
- # produced by group_stat(). For example, selecting ``pm25`` and
499
- # hourly means produces ``pm25_mean`` in the aggregated result.
500
- selected_y_columns = [column_map[c] for c in selected_y]
554
+ aggregate = AGGREGATES[input.aggregate()]
501
555
 
502
- # Summary.group_stat() creates the frequency column itself.
503
- # For example, frequency="hour" creates an ``hour`` column,
504
- # while an aggregate such as mean creates ``pm25_mean``.
505
- # Therefore the aggregated frequency column replaces the
506
- # originally selected X-axis column.
507
- if frequency is not None:
508
- aggregate = AGGREGATES[input.aggregate()]
509
- y_columns = _aggregated_value_columns(
510
- plot_df, selected_y_columns, aggregate
511
- )
512
- if selected_plot == "box":
513
- if selected_x not in column_map:
514
- return px.scatter(title="Select a categorical X variable for the boxplot")
515
- x_column = column_map[selected_x]
516
- else:
517
- x_column = frequency
556
+ def mapped(column):
557
+ original = column_map[column]
558
+ return f"{original}_{aggregate}" if frequency is not None else original
518
559
 
519
- if x_column not in plot_df.columns:
520
- return px.scatter(
521
- title=(
522
- f"Aggregation did not produce the expected "
523
- f"'{x_column}' column"
524
- )
525
- )
560
+ if selected_plot == "histogram":
561
+ x_column = mapped(selected_x[0])
562
+ y_columns = []
563
+ elif selected_plot in ("box", "bar"):
564
+ x_columns = [column_map[c] for c in selected_x]
565
+ x_column = x_columns[0]
566
+ y_columns = [mapped(selected_y[0])]
567
+ elif selected_plot == "scatter":
568
+ x_column = mapped(selected_x[0])
569
+ y_columns = [mapped(selected_y[0])]
526
570
  else:
527
- y_columns = [
528
- c for c in selected_y_columns if c in plot_df.columns
529
- ]
530
- x_column = column_map[selected_x]
531
-
532
- if not y_columns:
533
- return px.scatter(
534
- title="Selected variables are not available after aggregation"
535
- )
571
+ x_column = frequency if frequency is not None else column_map[selected_x[0]]
572
+ y_columns = [mapped(c) for c in selected_y]
573
+
574
+ required = [x_column, *y_columns]
575
+ if selected_plot in ("box", "bar"):
576
+ required.extend(x_columns)
577
+ if any(c not in plot_df.columns for c in required):
578
+ return px.scatter(title="Selected variables are unavailable after aggregation")
579
+
580
+ if selected_plot in ("box", "bar"):
581
+ plot_df = plot_df.copy()
582
+ plot_df["__plot_category"] = plot_df[x_columns].astype(
583
+ str).agg(" | ".join, axis=1)
584
+ x_column = "__plot_category"
536
585
 
537
586
  # Multiple grouping columns are supported by group_stat(). For
538
587
  # plotting, Plotly needs one color/category column, so combine the
@@ -546,74 +595,31 @@ def create_summary_app(
546
595
  )
547
596
 
548
597
  if selected_plot == "histogram":
549
- if group_columns:
550
- # With grouping, overlay one histogram per group on the
551
- # same axes. ``group`` is the color dimension rather
552
- # than the selected variable, so each group gets its own
553
- # histogram. Transparency makes overlapping groups
554
- # directly comparable.
555
- group_column = "__plot_group"
556
- long_df = plot_df[[group_column, *y_columns]].melt(
557
- id_vars=[group_column],
558
- var_name="variable",
559
- value_name="value",
560
- )
561
- fig = px.histogram(
562
- long_df,
563
- x="value",
564
- color=group_column,
565
- nbins=int(input.bins()),
566
- barmode="overlay",
567
- opacity=0.45,
568
- facet_row=("variable" if len(y_columns) > 1 else None),
569
- )
570
- # Keep the groups as the legend entries rather than the
571
- # selected Y variables.
572
- fig.update_traces(marker_line_width=0)
573
- else:
574
- long_df = plot_df[y_columns].melt(
575
- var_name="variable", value_name="value"
576
- )
577
- fig = px.histogram(
578
- long_df,
579
- x="value",
580
- color="variable",
581
- nbins=int(input.bins()),
582
- barmode="overlay" if len(
583
- y_columns) > 1 else "relative",
584
- opacity=0.65 if len(y_columns) > 1 else 1.0,
585
- )
598
+ fig = px.histogram(
599
+ plot_df,
600
+ x=x_column,
601
+ color="__plot_group" if group_columns else None,
602
+ nbins=int(input.bins()),
603
+ barmode="overlay",
604
+ opacity=0.45 if group_columns else 1.0,
605
+ )
586
606
  elif selected_plot == "box":
587
- # Boxplots are distributions of one numerical variable across
588
- # categories. X is therefore always the selected categorical
589
- # column and Y is exactly one numerical variable.
590
- if x_column not in categorical_columns:
591
- return px.scatter(
592
- title="Boxplots require a categorical variable on the X axis"
593
- )
594
- y_column = y_columns[0]
595
- if group_columns:
596
- fig = px.box(
597
- plot_df, x=x_column, y=y_column,
598
- color="__plot_group", points="outliers",
599
- )
600
- else:
601
- fig = px.box(
602
- plot_df, x=x_column, y=y_column, points="outliers"
603
- )
607
+ fig = px.box(
608
+ plot_df, x=x_column, y=y_columns[0],
609
+ color="__plot_group" if group_columns else None,
610
+ points="outliers",
611
+ )
604
612
  elif selected_plot == "bar":
605
- if not group_columns:
606
- fig = px.bar(plot_df, x=x_column,
607
- y=y_columns, barmode="group")
608
- else:
609
- group_column = "__plot_group"
610
- long_df = plot_df[[x_column, group_column, *y_columns]].melt(
611
- id_vars=[x_column, group_column], var_name="variable", value_name="value",
612
- )
613
- fig = px.bar(
614
- long_df, x=x_column, y="value", color=group_column,
615
- facet_row="variable" if len(y_columns) > 1 else None, barmode="group",
616
- )
613
+ fig = px.bar(
614
+ plot_df, x=x_column, y=y_columns[0],
615
+ color="__plot_group" if group_columns else None,
616
+ barmode="group",
617
+ )
618
+ elif selected_plot == "scatter":
619
+ fig = px.scatter(
620
+ plot_df, x=x_column, y=y_columns[0],
621
+ color="__plot_group" if group_columns else None,
622
+ )
617
623
  else:
618
624
  # ``identifier`` identifies an individual input monitor. It
619
625
  # must therefore be used as Plotly's line-group key even when
@@ -0,0 +1,386 @@
1
+ upas_units = {'AccelComplianceCnt': '#',
2
+ 'AccelComplianceHrs': 'hrs',
3
+ 'AccelT': '°C',
4
+ 'AccelX': 'mg',
5
+ 'AccelXMax': 'mg',
6
+ 'AccelXMin': 'mg',
7
+ 'AccelXVar': 'mg',
8
+ 'AccelY': 'mg',
9
+ 'AccelYMax': 'mg',
10
+ 'AccelYMin': 'mg',
11
+ 'AccelYVar': 'mg',
12
+ 'AccelZ': 'mg',
13
+ 'AccelZMax': 'mg',
14
+ 'AccelZMin': 'mg',
15
+ 'AccelZVar': 'mg',
16
+ 'AtmoAlt': 'm',
17
+ 'AtmoDensity': 'g L-1',
18
+ 'AtmoP': 'hPa',
19
+ 'AtmoRH': '%',
20
+ 'AtmoT': '°C',
21
+ 'BCS1': 'bool',
22
+ 'BCS2': 'bool',
23
+ 'BC_NPG': 'bool',
24
+ 'BFGenergy': 'integer',
25
+ 'BattVolt': 'V',
26
+ 'BatteryCharge': '%',
27
+ 'CO2': 'ppm',
28
+ 'Charging': 'bool',
29
+ 'DateTimeUTC': 'HH:MM:SS',
30
+ 'Dead': 'bool',
31
+ 'ExtPow': '-',
32
+ 'FLOWCTL': 's',
33
+ 'FdpT': '°C',
34
+ 'FilterDP': 'Pa',
35
+ 'GPSQual': '-',
36
+ 'GPSRT': 's',
37
+ 'GPSalt': 'm',
38
+ 'GPShDOP': '-',
39
+ 'GPSlat': 'decimalDegree',
40
+ 'GPSlon': 'decimalDegree',
41
+ 'GPSsat': '#',
42
+ 'GPSspeed': 'm s-1',
43
+ 'HighVisRaw': '-',
44
+ 'IRRaw': '-',
45
+ 'LUX': 'lux',
46
+ 'LowVisRaw': '-',
47
+ 'MFSVout': 'V',
48
+ 'MassFlow': 'g min-1',
49
+ 'MassFlowFactory': 'g min-1',
50
+ 'NOXRaw': '-',
51
+ 'OverallFlowFactory': 'L min-1',
52
+ 'OverallFlowOffset': 'L min-1',
53
+ 'OverallFlowRate': 'L min-1)',
54
+ 'PCB1T': '°C',
55
+ 'PCB2P': 'hPa',
56
+ 'PCB2T': '°C',
57
+ 'PM0_5NC': '# cm-3',
58
+ 'PM0_5NCVar': '# cm-3',
59
+ 'PM10MC': 'µg m-3',
60
+ 'PM10MCVar': 'µg m-3',
61
+ 'PM10NC': '# cm-3',
62
+ 'PM10NCVar': '# cm-3',
63
+ 'PM1MC': 'µg m-3',
64
+ 'PM1MCVar': 'µg m-3',
65
+ 'PM1NC': '# cm-3',
66
+ 'PM1NCVar': '# cm-3',
67
+ 'PM2_5MC': 'μg/m3',
68
+ 'PM2_5MCVar': 'µg m-3',
69
+ 'PM2_5NC': '# cm-3',
70
+ 'PM2_5NCVar': '# cm-3',
71
+ 'PM2_5SampledMass': 'µg',
72
+ 'PM2_5SampledMassFactory': 'μg',
73
+ 'PM2_5SampledMassOffset': 'μg',
74
+ 'PM4MC': 'µg m-3',
75
+ 'PM4MCVar': 'µg m-3',
76
+ 'PM4NC': '# cm-3',
77
+ 'PM4NCVar': '# cm-3',
78
+ 'PMFanErrorCnt': '#',
79
+ 'PMFanSpeedWarn': '#',
80
+ 'PMLaserErrorCnt': '#',
81
+ 'PMMeasCnt': '#',
82
+ 'PMReadingErrorCnt': '#',
83
+ 'PMtypicalParticleSize': 'µm',
84
+ 'PMtypicalParticleSizeVar': 'µm',
85
+ 'PT100R': 'Ω',
86
+ 'PumpPow1': 'integer',
87
+ 'PumpPow2': 'integer',
88
+ 'PumpV': 'V',
89
+ 'PumpingFlowFactory': 'L min-1',
90
+ 'PumpingFlowOffset': 'L min-1',
91
+ 'PumpingFlowRate': 'L min-1)',
92
+ 'PumpsON': 'bool',
93
+ 'RotX': 'mdeg s-1',
94
+ 'RotXMax': 'mdeg s-1',
95
+ 'RotXMin': 'mdeg s-1',
96
+ 'RotXVar': 'mdeg s-1',
97
+ 'RotY': 'mdeg s-1',
98
+ 'RotYMax': 'mdeg s-1',
99
+ 'RotYMin': 'mdeg s-1',
100
+ 'RotYVar': 'mdeg s-1',
101
+ 'RotZ': 'mdeg s-1',
102
+ 'RotZMax': 'mdeg s-1',
103
+ 'RotZMin': 'mdeg s-1',
104
+ 'RotZVar': 'mdeg s-1',
105
+ 'SCDRH': '%',
106
+ 'SCDT': '°C',
107
+ 'SD_DATAW': 's',
108
+ 'SD_HEADW': 's',
109
+ 'SampleTime': '',
110
+ 'SampledVolume': 'L',
111
+ 'SampledVolumeFactory': 'L',
112
+ 'SampledVolumeOffset': 'L',
113
+ 'StepCount': '#',
114
+ 'TPumpsOFF': 's',
115
+ 'TPumpsON': 's',
116
+ 'U12T': '°C',
117
+ 'U29P': '°C',
118
+ 'U29T': '°C',
119
+ 'UVRaw': '-',
120
+ 'UVindex': '-',
121
+ 'UnixTime': 's',
122
+ 'VOCRaw': '-',
123
+ 'XDown': '%',
124
+ 'Xup': '%',
125
+ 'Ydown': '%',
126
+ 'Yup': '%',
127
+ 'Zdown': '%',
128
+ 'Zup': '%',
129
+ 'cartridgeid': '-',
130
+ 'end': 'YYYY-MM-DDTHH:MM:SS',
131
+ 'index': '-',
132
+ 'length': 'days HH:MM:SS',
133
+ 'pm25': 'µg m-3',
134
+ 'pm25correctedrh': 'µg m-3',
135
+ 'start': 'YYYY-MM-DDTHH:MM:SS',
136
+ 'time': 'YYYY-MM-DDTHH:MM:SS',
137
+ 'upasid': '-',
138
+ 'v3_3': 'V',
139
+ 'v5': 'V'}
140
+
141
+ upas_datatype = {'AccelComplianceCnt': 'integer',
142
+ 'AccelComplianceHrs': 'timedelta',
143
+ 'AccelT': 'numeric',
144
+ 'AccelX': 'numeric',
145
+ 'AccelXMax': 'numeric',
146
+ 'AccelXMin': 'numeric',
147
+ 'AccelXVar': 'numeric',
148
+ 'AccelY': 'numeric',
149
+ 'AccelYMax': 'numeric',
150
+ 'AccelYMin': 'numeric',
151
+ 'AccelYVar': 'numeric',
152
+ 'AccelZ': 'numeric',
153
+ 'AccelZMax': 'numeric',
154
+ 'AccelZMin': 'numeric',
155
+ 'AccelZVar': 'numeric',
156
+ 'AtmoAlt': 'numeric',
157
+ 'AtmoDensity': 'numeric',
158
+ 'AtmoP': 'numeric',
159
+ 'AtmoRH': 'numeric',
160
+ 'AtmoT': 'numeric',
161
+ 'BCS1': 'bool',
162
+ 'BCS2': 'bool',
163
+ 'BC_NPG': 'bool',
164
+ 'BFGenergy': 'integer',
165
+ 'BattVolt': 'numeric',
166
+ 'BatteryCharge': '%',
167
+ 'CO2': 'numeric',
168
+ 'Charging': 'bool',
169
+ 'DateTimeUTC': 'datetime',
170
+ 'Dead': 'bool',
171
+ 'ExtPow': 'bool',
172
+ 'FLOWCTL': 'timedelta',
173
+ 'FdpT': 'numeric',
174
+ 'FilterDP': 'numeric',
175
+ 'GPSQual': 'text',
176
+ 'GPSRT': 'timedelta',
177
+ 'GPSalt': 'numeric',
178
+ 'GPShDOP': '',
179
+ 'GPSlat': 'numeric',
180
+ 'GPSlon': 'numeric',
181
+ 'GPSsat': 'integer',
182
+ 'GPSspeed': 'numeric',
183
+ 'HighVisRaw': '',
184
+ 'IRRaw': '',
185
+ 'LUX': 'numeric',
186
+ 'LowVisRaw': '',
187
+ 'MFSVout': 'numeric',
188
+ 'MassFlow': 'numeric',
189
+ 'MassFlowFactory': 'numeric',
190
+ 'NOXRaw': 'numeric',
191
+ 'OverallFlowFactory': 'numeric',
192
+ 'OverallFlowOffset': 'numeric',
193
+ 'OverallFlowRate': 'numeric',
194
+ 'PCB1T': 'numeric',
195
+ 'PCB2P': 'numeric',
196
+ 'PCB2T': 'numeric',
197
+ 'PM0_5NC': 'integer',
198
+ 'PM0_5NCVar': 'numeric',
199
+ 'PM10MC': 'numeric',
200
+ 'PM10MCVar': 'numeric',
201
+ 'PM10NC': 'numeric',
202
+ 'PM10NCVar': 'numeric',
203
+ 'PM1MC': 'numeric',
204
+ 'PM1MCVar': 'numeric',
205
+ 'PM1NC': 'numeric',
206
+ 'PM1NCVar': 'numeric',
207
+ 'PM2_5MC': 'numeric',
208
+ 'PM2_5MCVar': 'numeric',
209
+ 'PM2_5NC': 'numeric',
210
+ 'PM2_5NCVar': 'numeric',
211
+ 'PM2_5SampledMass': 'numeric',
212
+ 'PM2_5SampledMassFactory': 'numeric',
213
+ 'PM2_5SampledMassOffset': 'numeric',
214
+ 'PM4MC': 'numeric',
215
+ 'PM4MCVar': 'numeric',
216
+ 'PM4NC': 'numeric',
217
+ 'PM4NCVar': 'numeric',
218
+ 'PMFanErrorCnt': 'numeric',
219
+ 'PMFanSpeedWarn': 'bool',
220
+ 'PMLaserErrorCnt': 'numeric',
221
+ 'PMMeasCnt': 'numeric',
222
+ 'PMReadingErrorCnt': 'numeric',
223
+ 'PMtypicalParticleSize': 'numeric',
224
+ 'PMtypicalParticleSizeVar': 'numeric',
225
+ 'PT100R': 'numeric',
226
+ 'PumpPow1': 'numeric',
227
+ 'PumpPow2': 'numeric',
228
+ 'PumpV': 'numeric',
229
+ 'PumpingFlowFactory': 'numeric',
230
+ 'PumpingFlowOffset': 'numeric',
231
+ 'PumpingFlowRate': 'numeric',
232
+ 'PumpsON': 'bool',
233
+ 'RotX': 'numeric',
234
+ 'RotXMax': 'numeric',
235
+ 'RotXMin': 'numeric',
236
+ 'RotXVar': 'numeric',
237
+ 'RotY': 'numeric',
238
+ 'RotYMax': 'numeric',
239
+ 'RotYMin': 'numeric',
240
+ 'RotYVar': 'numeric',
241
+ 'RotZ': 'numeric',
242
+ 'RotZMax': 'numeric',
243
+ 'RotZMin': 'numeric',
244
+ 'RotZVar': 'numeric',
245
+ 'SCDRH': 'numeric',
246
+ 'SCDT': 'numeric',
247
+ 'SD_DATAW': 'timedelta',
248
+ 'SD_HEADW': 'timedelta',
249
+ 'SampleTime': 'datetime',
250
+ 'SampledVolume': 'numeric',
251
+ 'SampledVolumeFactory': 'numeric',
252
+ 'SampledVolumeOffset': 'numeric',
253
+ 'StepCount': 'numeric',
254
+ 'TPumpsOFF': 'bool',
255
+ 'TPumpsON': 'bool',
256
+ 'U12T': 'numeric',
257
+ 'U29P': 'numeric',
258
+ 'U29T': 'numeric',
259
+ 'UVRaw': 'numeric',
260
+ 'UVindex': 'numeric',
261
+ 'UnixTime': 'datetime',
262
+ 'VOCRaw': 'numeric',
263
+ 'XDown': 'numeric',
264
+ 'Xup': 'numeric',
265
+ 'Ydown': 'numeric',
266
+ 'Yup': 'numeric',
267
+ 'Zdown': 'numeric',
268
+ 'Zup': 'numeric',
269
+ 'end': 'datetime',
270
+ 'length': 'timedelta',
271
+ 'pm25': 'numeric',
272
+ 'pm25correctedrh': 'numeric',
273
+ 'start': 'datetime',
274
+ 'time': 'datetime',
275
+ 'v3_3': 'numeric',
276
+ 'v5': 'numeric'}
277
+
278
+ lascar_units = {"CO(ppm)":"ppm"}
279
+
280
+ lascar_datatype = {"CO(ppm)": "numeric",
281
+ 'start': "datetime",
282
+ 'end': "datetime",
283
+ 'length': "timedelta",
284
+ 'time': "datetime"}
285
+
286
+ purple_units = {'mac_address':"-",
287
+ 'firmware_ver':"-",
288
+ 'hardware':"-",
289
+ 'current_temp_f':"F",
290
+ 'current_humidity':"%",
291
+ 'current_dewpoint_f':"F",
292
+ 'pressure':"mBar",
293
+ 'adc':"",
294
+ 'mem':"",
295
+ 'rssi':"",
296
+ 'uptime':"",
297
+ 'pm1_0_cf_1': "μg m-3",
298
+ 'pm2_5_cf_1': "μg m-3",
299
+ 'pm10_0_cf_1': "μg m-3",
300
+ 'pm1_0_atm': "μg m-3",
301
+ 'pm2_5_atm': "μg m-3",
302
+ 'pm10_0_atm': "μg m-3",
303
+ 'pm2.5_aqi_cf_1': "μg m-3",
304
+ 'pm2.5_aqi_atm': "μg m-3",
305
+ 'p_0_3_um':"#",
306
+ 'p_0_5_um':"#",
307
+ 'p_1_0_um':"#",
308
+ 'p_2_5_um':"#",
309
+ 'p_5_0_um':"#",
310
+ 'p_10_0_um':"#",
311
+ 'pm1_0_cf_1_b': "μg m-3",
312
+ 'pm2_5_cf_1_b': "μg m-3",
313
+ 'pm10_0_cf_1_b': "μg m-3",
314
+ 'pm1_0_atm_b': "μg m-3",
315
+ 'pm2_5_atm_b': "μg m-3",
316
+ 'pm10_0_atm_b': "μg m-3",
317
+ 'pm2.5_aqi_cf_1_b': "μg m-3",
318
+ 'pm2.5_aqi_atm_b': "μg m-3",
319
+ 'p_0_3_um_b':"#",
320
+ 'p_0_5_um_b':"#",
321
+ 'p_1_0_um_b':"#",
322
+ 'p_2_5_um_b':"#",
323
+ 'p_5_0_um_b':"#",
324
+ 'p_10_0_um_b':"#",
325
+ 'gas':"",
326
+ 'pm_adj': "μg m-3"}
327
+
328
+ purple_datatype = {'mac_address': "text",
329
+ 'firmware_ver': "text",
330
+ 'hardware': "text",
331
+ 'current_temp_f': "numeric",
332
+ 'current_humidity': "numeric",
333
+ 'current_dewpoint_f': "numeric",
334
+ 'pressure': "numeric",
335
+ 'adc': "text",
336
+ 'mem': "text",
337
+ 'rssi': "text",
338
+ 'uptime': "text",
339
+ 'pm1_0_c': "numeric",
340
+ 'PumpingFlowFactory': 'numeric',
341
+ 'PumpingFlowOffset': 'numeric',
342
+ 'PumpingFlowRate': 'numeric',
343
+ 'PumpsON': 'bool',
344
+ 'RotX': 'numeric',
345
+ 'RotXMax': 'numeric',
346
+ 'RotXMin': 'numeric',
347
+ 'RotXVar': 'numeric',
348
+ 'RotY': 'numeric',
349
+ 'RotYMax': 'numeric',
350
+ 'RotYMin': 'numeric',
351
+ 'RotYVar': 'numeric',
352
+ 'RotZ': 'numeric',
353
+ 'pm1_0_cf_1': "numeric",
354
+ 'pm2_5_cf_1': "numeric",
355
+ 'pm10_0_cf_1': "numeric",
356
+ 'pm1_0_atm': "numeric",
357
+ 'pm2_5_atm': "numeric",
358
+ 'pm10_0_atm': "numeric",
359
+ 'pm2.5_aqi_cf_1': "numeric",
360
+ 'pm2.5_aqi_atm': "numeric",
361
+ 'p_0_3_um': "numeric",
362
+ 'p_0_5_um': "numeric",
363
+ 'p_1_0_um': "numeric",
364
+ 'p_2_5_um': "numeric",
365
+ 'p_5_0_um': "numeric",
366
+ 'p_10_0_um': "numeric",
367
+ 'pm1_0_cf_1_b': "numeric",
368
+ 'pm2_5_cf_1_b': "numeric",
369
+ 'pm10_0_cf_1_b': "numeric",
370
+ 'pm1_0_atm_b': "numeric",
371
+ 'pm2_5_atm_b': "numeric",
372
+ 'pm10_0_atm_b': "numeric",
373
+ 'pm2.5_aqi_cf_1_b': "numeric",
374
+ 'pm2.5_aqi_atm_b': "numeric",
375
+ 'p_0_3_um_b': "numeric",
376
+ 'p_0_5_um_b': "numeric",
377
+ 'p_1_0_um_b': "numeric",
378
+ 'p_2_5_um_b': "numeric",
379
+ 'p_5_0_um_b': "numeric",
380
+ 'p_10_0_um_b': "numeric",
381
+ 'gas': "text",
382
+ 'pm_adj': "numeric",
383
+ 'start':"datetime",
384
+ 'end':"datetime",
385
+ 'length':"timedelta",
386
+ 'time':"datetime"}
@@ -1,155 +0,0 @@
1
- upas_units= {'PumpingFlowRate':"",
2
- 'OverallFlowRate':"",
3
- 'SampledVolume':"",
4
- 'FilterDP':"",
5
- 'BatteryCharge':"",
6
- 'AtmoT':"",
7
- 'AtmoP':"",
8
- 'AtmoRH':"",
9
- 'AtmoDensity':"",
10
- 'AtmoAlt':"",
11
- 'GPSQual':"",
12
- 'GPSlat':"",
13
- 'GPSlon':"",
14
- 'GPSalt':"",
15
- 'GPSsat':"",
16
- 'GPSspeed':"",
17
- 'GPShDOP':"",
18
- 'AccelX':"",
19
- 'AccelXVar':"",
20
- 'AccelXMin':"",
21
- 'AccelXMax':"",
22
- 'AccelY':"",
23
- 'AccelYVar':"",
24
- 'AccelYMin':"",
25
- 'AccelYMax':"",
26
- 'AccelZ':"",
27
- 'AccelZVar':"",
28
- 'AccelZMin':"",
29
- 'AccelZMax':"",
30
- 'RotX':"",
31
- 'RotXVar':"",
32
- 'RotXMin':"",
33
- 'RotXMax':"",
34
- 'RotY':"",
35
- 'RotYVar':"",
36
- 'RotYMin':"",
37
- 'RotYMax':"",
38
- 'RotZ':"",
39
- 'RotZVar':"",
40
- 'RotZMin':"",
41
- 'RotZMax':"",
42
- 'Xup':"",
43
- 'XDown':"",
44
- 'Yup':"",
45
- 'Ydown':"",
46
- 'Zup':"",
47
- 'Zdown':"",
48
- 'StepCount':"",
49
- 'LUX':"",
50
- 'UVindex':"",
51
- 'HighVisRaw':"",
52
- 'LowVisRaw':"",
53
- 'IRRaw':"",
54
- 'UVRaw':"",
55
- 'PMMeasCnt':"",
56
- 'PM1MC':"",
57
- 'PM1MCVar':"",
58
- 'PM2_5MC': "μg/m3",
59
- 'PM2_5MCVar':"",
60
- 'PM4MC':"",
61
- 'PM4MCVar':"",
62
- 'PM10MC':"",
63
- 'PM10MCVar':"",
64
- 'PM0_5NC':"",
65
- 'PM0_5NCVar':"",
66
- 'PM1NC':"",
67
- 'PM1NCVar':"",
68
- 'PM2_5NC':"",
69
- 'PM2_5NCVar':"",
70
- 'PM4NC':"",
71
- 'PM4NCVar':"",
72
- 'PM10NC':"",
73
- 'PM10NCVar':"",
74
- 'PMtypicalParticleSize':"",
75
- 'PMtypicalParticleSizeVar':"",
76
- 'PM2_5SampledMass':"",
77
- 'PMReadingErrorCnt':"",
78
- 'PMFanErrorCnt':"",
79
- 'PMLaserErrorCnt':"",
80
- 'PMFanSpeedWarn':"",
81
- 'PCB1T':"",
82
- 'PCB2T':"",
83
- 'FdpT':"",
84
- 'AccelT':"",
85
- 'PT100R':"",
86
- 'PCB2P':"",
87
- 'PumpPow1':"",
88
- 'PumpPow2':"",
89
- 'PumpV':"",
90
- 'MassFlow':"",
91
- 'MFSVout':"",
92
- 'BFGenergy':"",
93
- 'BattVolt':"",
94
- 'v3_3':"",
95
- 'v5':"",
96
- 'PumpsON':"",
97
- 'Dead':"",
98
- 'BCS1':"",
99
- 'BCS2':"",
100
- 'BC_NPG':"",
101
- 'FLOWCTL':"",
102
- 'GPSRT':"",
103
- 'SD_DATAW':"",
104
- 'SD_HEADW':"",
105
- 'TPumpsOFF':"",
106
- 'TPumpsON':"",
107
- 'CO2':"",
108
- 'SCDT':"",
109
- 'SCDRH':"",
110
- 'VOCRaw':"",
111
- 'NOXRaw':""}
112
-
113
- lascar_units = {"CO(ppm)":"ppm"}
114
-
115
- purple_units = {'mac_address':"",
116
- 'firmware_ver':"",
117
- 'hardware':"",
118
- 'current_temp_f':"",
119
- 'current_humidity':"",
120
- 'current_dewpoint_f':"",
121
- 'pressure':"",
122
- 'adc':"",
123
- 'mem':"",
124
- 'rssi':"",
125
- 'uptime':"",
126
- 'pm1_0_cf_1':"",
127
- 'pm2_5_cf_1':"",
128
- 'pm10_0_cf_1':"",
129
- 'pm1_0_atm':"",
130
- 'pm2_5_atm':"",
131
- 'pm10_0_atm':"",
132
- 'pm2.5_aqi_cf_1':"",
133
- 'pm2.5_aqi_atm':"",
134
- 'p_0_3_um':"",
135
- 'p_0_5_um':"",
136
- 'p_1_0_um':"",
137
- 'p_2_5_um':"",
138
- 'p_5_0_um':"",
139
- 'p_10_0_um':"",
140
- 'pm1_0_cf_1_b':"",
141
- 'pm2_5_cf_1_b':"",
142
- 'pm10_0_cf_1_b':"",
143
- 'pm1_0_atm_b':"",
144
- 'pm2_5_atm_b':"",
145
- 'pm10_0_atm_b':"",
146
- 'pm2.5_aqi_cf_1_b':"",
147
- 'pm2.5_aqi_atm_b':"",
148
- 'p_0_3_um_b':"",
149
- 'p_0_5_um_b':"",
150
- 'p_1_0_um_b':"",
151
- 'p_2_5_um_b':"",
152
- 'p_5_0_um_b':"",
153
- 'p_10_0_um_b':"",
154
- 'gas':"",
155
- 'pm_adj': "μg/m3"}
File without changes
File without changes
File without changes