Skip to content

Commit

Permalink
Merge pull request #72 from LDAR-Sim/FEAT_external_sensor_addition
Browse files Browse the repository at this point in the history
<Feat> external sensor addition
  • Loading branch information
SEAJang authored Jul 11, 2023
2 parents a33625a + 20b0712 commit 0c1b3c8
Show file tree
Hide file tree
Showing 11 changed files with 496 additions and 6 deletions.
152 changes: 152 additions & 0 deletions LDAR_Sim/external_sensors/METEC_NO_WIND.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""
An alternative sensor, specifically built to replicate the probability of detection
curves provided by a METEC report, which does not factor in wind speeds
"""
# ------------------------------------------------------------------------------
# Program: The LDAR Simulator (LDAR-Sim)
# File: methods.deployment.OGI_camera_zim
# Purpose: OGI company specific deployment classes and methods based on zimmerle (2020)
#
# Copyright (C) 2018-2021 Intelligent Methane Monitoring and Management System (IM3S) Group
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the MIT License as published
# by the Free Software Foundation, version 3.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# MIT License for more details.

# You should have received a copy of the MIT License
# along with this program. If not, see <https://opensource.org/licenses/MIT>.
#
# ------------------------------------------------------------------------------
import numpy as np
from utils.attribution import update_tag
from methods.funcs import measured_rate


def detect_emissions(self, site, covered_leaks, covered_equipment_rates, covered_site_rate,
site_rate, venting, equipment_rates): # pylint: disable=unused-argument
"""
An alternative sensor, specifically built to replicate the probability of detection
curves provided by a METEC report, which does not factor in wind speeds
Utilizes 3 values set as the MDL:
mdl = [a, b, c]
where
a and b : utilize for the PoD curve variables
c : represents the floor/minimum cutoff value of the leak rates that the sensor can detect
PoD = 1 / (1 + e ^ (a - b * r ))
a = first MDL value
b = second MDL value
r = emission rate
Args:
site (site obj): Site in which crew is working at
covered_leaks (list): list of leak objects that can be detected by the crew
covered_equipment_rates (list): list of equipment leak rates that can be
detected by the crew
covered_site_rate (float): total site emissions from leaks that are observable
from a crew
site_rate (float): total site emissions from leaks all leaks at site
venting (float): total site emissions from venting
equipment_rates (list): list of equipment leak rates for each equipment group
Returns:
site report (dict):
site (site obj): same as input
leaks_present (list): same as covered leaks input
site_true_rate (float): same as site_rate
site_measured_rate (float): total emis from all leaks measured
equip_measured_rates (list): total of all leaks measured for each equip group
venting (float): same as input
found_leak (boolean): Did the crew find at least one leak at the site
"""

missed_leaks_str = '{}_missed_leaks'.format(self.config['label'])
equip_measured_rates = []
site_measured_rate = 0
found_leak = False
n_leaks = len(covered_leaks)
mdl = self.config['sensor']['MDL']

if self.config["measurement_scale"] == "site":
# factor of 3.6 converts g/s to kg/h
rate = covered_site_rate * 3.6
prob_detect = 1/(1+np.exp(mdl[0]-mdl[1]*rate))
if prob_detect >= 1:
prob_detect = 1
if rate < (mdl[2]*3.6):
site[missed_leaks_str] += n_leaks
self.timeseries[missed_leaks_str][self.state['t'].current_timestep] += n_leaks
elif np.random.binomial(1, prob_detect):
found_leak = True
site_measured_rate = measured_rate(
covered_site_rate, self.config['sensor']['QE'])
else:
site[missed_leaks_str] += n_leaks
self.timeseries[missed_leaks_str][self.state['t'].current_timestep] += n_leaks
elif self.config["measurement_scale"] == "equipment":
for rate in covered_equipment_rates:
m_rate = measured_rate(rate, self.config['sensor']['QE'])
rate = m_rate * 3.6
prob_detect = 1 / \
(1+np.exp(mdl[0]-mdl[1]*rate))
if prob_detect >= 1:
prob_detect = 1
if rate < (mdl[2]*3.6):
m_rate = 0
elif np.random.binomial(1, prob_detect):
found_leak = True
else:
m_rate = 0
equip_measured_rates.append(m_rate)
site_measured_rate += m_rate
if not found_leak:
site[missed_leaks_str] += n_leaks
self.timeseries[missed_leaks_str][self.state['t'].current_timestep] += n_leaks
elif self.config['measurement_scale'] == 'component':
for leak in covered_leaks:
rate = leak['rate'] * 3.6
prob_detect = 1 / \
(1+np.exp(mdl[0]-mdl[1]*rate))
if prob_detect >= 1:
prob_detect = 1
if rate < (mdl[2]*3.6):
site[missed_leaks_str] += 1
self.timeseries[missed_leaks_str][self.state['t'].current_timestep] += 1
elif np.random.binomial(1, prob_detect):
found_leak = True
meas_rate = measured_rate(
leak['rate'], self.config['sensor']['QE'])
is_new_leak = update_tag(
leak,
meas_rate,
site,
self.timeseries,
self.state['t'],
self.config['label'],
self.id,
self.parameters
)
if is_new_leak:
site_measured_rate += meas_rate
else:
site[missed_leaks_str] += 1
self.timeseries[missed_leaks_str][self.state['t'].current_timestep] += 1
site_dict = {
'site': site,
'leaks_present': covered_leaks,
'site_true_rate': site_rate,
'site_measured_rate': site_measured_rate,
'equip_measured_rates': equip_measured_rates,
'vent_rate': venting,
'found_leak': found_leak,
}
return site_dict
121 changes: 121 additions & 0 deletions LDAR_Sim/external_sensors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# External Sensor Coding Practices

This document provides coding guidelines and best practices for developing external sensors/plugins for use with LDAR-Sim. Following these practices will ensure consistency, readability, and maintainability of the codebase.

## Table of Contents

- [External Sensor Coding Practices](#external-sensor-coding-practices)
- [Table of Contents](#table-of-contents)
- [General Guidelines](#general-guidelines)
- [File Structure](#file-structure)
- [Naming Conventions](#naming-conventions)
- [Code Formatting](#code-formatting)
- [Documentation](#documentation)
- [Error Handling](#error-handling)
- [Testing](#testing)
- [Detect Emissions Function Requirement](#detect-emissions-function-requirement)

## General Guidelines

- Keep the code modular, well-organized, and maintainable.
- Follow the [Python PEP 8 Style Guide](https://www.python.org/dev/peps/pep-0008/) for code style and formatting.
- If possible use the autopep8 auto formatting
- Write code that is concise, readable, and self-explanatory.
- Avoid unnecessary code duplication; favor code reuse.

## File Structure

- Organize your plugin files such that they are located in the `external_sensors` folder
- Each sensor should be one file
- Consider using a package structure for larger plugins.

## Naming Conventions

- Use descriptive and meaningful names for variables, functions, classes, and modules.
- Follow the Python naming conventions: use lowercase with underscores for variables and functions (`my_variable`, `my_function`), and use CamelCase for classes (`MyClass`).
- Avoid single-character variable names except for simple loop counters.

## Code Formatting

- Use consistent and readable code formatting.
- Keep lines within a reasonable length (80-120 characters).
- Use blank lines and proper spacing to enhance readability.
- Follow appropriate naming conventions for constants and global variables.
- If possible use Black as a formatter.

## Documentation

- Provide clear and concise comments and docstrings. See [Detect Emissions Function Requirement](#detect-emissions-function-requirement) for more details.
- Include a high-level overview of the plugin's purpose and functionality.
- Document public interfaces, classes, and functions.
- Add inline comments where necessary to explain complex code logic.
- Include code examples or usage instructions when necessary.

- Within the docstring, be sure to include the probability of detection curve that the sensor should be replicating. Additionally, add in additional values that the function requires as inputs. For example:

```python

def detect_emissions(self, site, covered_leaks, covered_equipment_rates, covered_site_rate,
site_rate, venting, equipment_rates):
"""
An alternative sensor
Utilizes 3 values set as the MDL:
mdl = [a, b, c]
where
a and b are utilize for the POD curve variables
c represents the floor/minimum cutoff value of the leak rates
"""
```

## Error Handling

- Use proper error handling techniques to handle exceptions.
- Catch specific exceptions instead of using broad `except` clauses.
- Log or report errors appropriately to aid troubleshooting and debugging.
- Handle exceptions gracefully to avoid crashes or unexpected behavior.

## Testing

- Write unit tests to verify the correctness of your plugin.
- Test all major functionalities and edge cases.
- Use test frameworks like `unittest` or `pytest`.
- Ensure the tests are easily runnable and provide clear output.

## Detect Emissions Function Requirement

The external sensor function should have the following signature and functionality:

```python
def detect_emissions(site, covered_leaks, covered_equipment_rates, covered_site_rate,
site_rate, venting, equipment_rates):
"""
Perform sensor measurements and generate a site report.
Args:
site (site obj): Site in which crew is working at
covered_leaks (list): List of leak objects that can be detected by the crew
covered_equipment_rates (list): List of equipment leak rates that can be
detected by the crew
covered_site_rate (float): Total site emissions from leaks that are observable
from a crew
site_rate (float): Total site emissions from all leaks at the site
venting (float): Total site emissions from venting
equipment_rates (list): List of equipment leak rates for each equipment group
Returns:
site_report (dict):
'site' (site obj): Same as input
'leaks_present' (list): Same as covered_leaks input
'site_true_rate' (float): Same as site_rate
'site_measured_rate' (float): Total emissions from all leaks measured
'equip_measured_rates' (list): Total emissions from all leaks measured for each equipment group
'venting' (float): Same as input
'found_leak' (boolean): Indicates if the crew found at least one leak at the site
"""
# Implement the sensor function logic here
# ...
# Return the site report
```

**Note:** Rates provided by LDAR-Sim are in g/s, however resulting rate should be in kg/hr.
Empty file.
18 changes: 18 additions & 0 deletions LDAR_Sim/simulations/M_aircraft_ex_sensor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
parameter_level: method
version: '2.0'
label: aircraft_ex_sensor
deployment_type: mobile
measurement_scale: equipment
is_follow_up: False
sensor:
mod_loc: METEC_NO_WIND
MDL: [4,5,0]
cost:
per_site: 200
coverage:
spatial: 0.9
temporal: 0.9
t_bw_sites:
vals: [5]
RS: 4
time: 1
6 changes: 6 additions & 0 deletions LDAR_Sim/simulations/P_aircraft_ex_sensor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
program_name: P_aircraft_ex_sensor
parameter_level: program
version: '2.0'
method_labels:
- aircraft_ex_sensor
- OGI_FU
20 changes: 15 additions & 5 deletions LDAR_Sim/src/ldar_sim_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import json
import multiprocessing as mp
import os
import sys
import shutil
from pathlib import Path

Expand Down Expand Up @@ -51,9 +52,14 @@

# Get route directory , which is parent folder of ldar_sim_main file
# Set current working directory directory to root directory
root_dir = Path(os.path.dirname(os.path.realpath(__file__))).parent
root_dir = Path(__file__).resolve().parent.parent
os.chdir(root_dir)

src_dir = root_dir / 'src'
sys.path.insert(1, str(src_dir))
ext_sens_dir = root_dir / 'external_sensors'
sys.path.append(str(ext_sens_dir))

# --- Retrieve input parameters and parse ---
parameter_filenames = files_from_args(root_dir)
input_manager = InputManager()
Expand All @@ -62,7 +68,8 @@
parameter_filenames['parameter_files'])
out_dir = get_abs_path(parameter_filenames['out_dir'])
else:
sim_params = input_manager.read_and_validate_parameters(parameter_filenames)
sim_params = input_manager.read_and_validate_parameters(
parameter_filenames)
out_dir = get_abs_path(sim_params['output_directory'])

# --- Assign local variabls
Expand Down Expand Up @@ -91,7 +98,8 @@
else:
generator_dir = None
# --- Create simulations ---
simulations = create_sims(sim_params, programs, generator_dir, in_dir, out_dir, input_manager)
simulations = create_sims(sim_params, programs,
generator_dir, in_dir, out_dir, input_manager)

# --- Run simulations (in parallel) --
with mp.Pool(processes=sim_params['n_processes']) as p:
Expand All @@ -105,7 +113,8 @@
# Create a data object...
if has_ref & has_base:
print("....Generating cost mitigation outputs")
cost_mitigation = cost_mitigation(sim_outputs, ref_program, base_program, out_dir)
cost_mitigation = cost_mitigation(
sim_outputs, ref_program, base_program, out_dir)
reporting_data = BatchReporting(
out_dir, sim_params['start_date'], ref_program, base_program)
if sim_params['n_simulations'] > 1:
Expand All @@ -115,7 +124,8 @@
reporting_data.batch_report()
reporting_data.batch_plots()
else:
print('No reference or base program input...skipping batch reporting and economics.')
print(
'No reference or base program input...skipping batch reporting and economics.')

# Generate output table
print("....Exporting summary statistic tables")
Expand Down
9 changes: 8 additions & 1 deletion LDAR_Sim/testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Module Unique Identifiers have been outlined below:
- out_processing: 06
- utils: 07
- weather: 08
- external_sensors: 09

## Required installation

Expand All @@ -33,7 +34,7 @@ Before running Pytest coverage, install
pip install pytest-cov
```

## Code Coverage Gude
## Code Coverage Guide

Pytest-cov allows for generation coverage reports for LDAR-Sim

Expand Down Expand Up @@ -78,3 +79,9 @@ Listed below are some other options for users:
The line coverage report can be found in html format inside the testing/coverage/line folder
For more details on pytest code coverage options, reference the documentation for pytest-cov at: <https://pytest-cov.readthedocs.io/en/latest/index.html>
## Pytest configuration
LDAR-Sim is structured in such a away that it uses relative pathways for module imports. This can cause troubles with unit tests, where it expects the full pathways for imports. A solution to this problem is to create a pytest.ini file, that dictates the relative python paths, based on the root folder.
For more details, reference the documentation for pytest configuration at: <https://docs.pytest.org/en/7.1.x/reference/reference.html#configuration-options>
Empty file.
Loading

0 comments on commit 0c1b3c8

Please sign in to comment.