formatting issues and introduction of pelt algorithm to display suggested transition times

This commit is contained in:
Luis Aleixo 2023-07-26 17:16:46 +02:00
parent 1c8775c807
commit b0d94f2318
3 changed files with 81 additions and 13 deletions

View file

@ -20,6 +20,7 @@ import uuid
import zlib
import matplotlib.pyplot as plt
import numpy as np
import ruptures as rpt
import jinja2
import loky
@ -352,14 +353,46 @@ class CO2Data(BaseRequestHandler):
"""
pass
def generate_ventilation_plot(self, CO2Data, transition_times = None, ventilation_values = None):
def find_change_points_with_pelt(self, CO2_data: dict):
"""
Perform change point detection using Pelt algorithm from ruptures library with pen=15.
Returns a list of tuples containing (index, X-axis value) for the detected significant changes.
"""
times = CO2_data['times']
CO2_values = CO2_data['CO2']
if len(times) != len(CO2_values):
raise ValueError("times and CO2 values must have the same length.")
# Convert the input lists to numpy arrays for use with the ruptures library
times_np = np.array(times)
CO2_np = np.array(CO2_values)
# Define the model for change point detection (Radial Basis Function kernel)
model = "rbf"
# Fit the Pelt algorithm to the data with the specified model
algo = rpt.Pelt(model=model).fit(CO2_np)
# Predict change points using the Pelt algorithm with a penalty value of 15
result = algo.predict(pen=15)
return [times_np[idx] for idx in result[:-1]]
def generate_ventilation_plot(self, CO2_data: dict, transition_times: typing.Optional[list] = None, ventilation_values: typing.Optional[list] = None):
times = CO2_data['times']
CO2_data = CO2_data['CO2']
fig = plt.figure(figsize=(7, 4), dpi=110)
plt.plot(CO2Data['times'], CO2Data['CO2'])
if (transition_times and ventilation_values):
for index, time in enumerate(transition_times[:-1]):
plt.plot(times, CO2_data)
if (transition_times):
for index, time in enumerate(transition_times):
plt.axvline(x = time, color = 'grey', linewidth=0.5, linestyle='--')
y_location = (CO2Data['CO2'][min(range(len(CO2Data['times'])), key=lambda i: abs(CO2Data['times'][i]-time))])
plt.text(x = time + 0.04, y = y_location, s=round(ventilation_values[index], 2))
if ventilation_values:
y_location = (CO2_data[min(range(len(times)), key=lambda i: abs(times[i]-time))])
plt.text(x = time + 0.04, y = y_location, s="{:.2g}".format(ventilation_values[index]))
plt.xlabel('Time of day')
plt.ylabel('Concentration (ppm)')
return img2base64(_figure2bytes(fig))
@ -377,8 +410,8 @@ class CO2Data(BaseRequestHandler):
self.finish(json.dumps(response_json))
return
if endpoint == 'plot':
self.finish({'CO2_plot': self.generate_ventilation_plot(form.CO2_data)})
if endpoint.rstrip('/') == 'plot':
self.finish({'CO2_plot': self.generate_ventilation_plot(form.CO2_data, self.find_change_points_with_pelt(form.CO2_data))})
else:
executor = loky.get_reusable_executor(
max_workers=self.settings['handler_worker_pool_size'],
@ -392,7 +425,7 @@ class CO2Data(BaseRequestHandler):
result = dict(report.CO2_fit_params())
result['fitting_ventilation_type'] = form.fitting_ventilation_type
result['transition_times'] = report.ventilation_transition_times
result['CO2_plot'] = self.generate_ventilation_plot(form.CO2_data, report.ventilation_transition_times, result['ventilation_values'])
result['CO2_plot'] = self.generate_ventilation_plot(form.CO2_data, report.ventilation_transition_times[:-1], result['ventilation_values'])
self.finish(result)

View file

@ -194,7 +194,37 @@ function validateCO2Form() {
// validate input format
try {
const parsedValue = JSON.parse(element.value);
if (!Array.isArray(parsedValue)) {
if (Array.isArray(parsedValue)) {
if (parsedValue.length <= 1) {
insertErrorFor(
$("#DIVCO2_fitting_result"),
`'${element.name}' must have more than one element.<br />`
);
submit = false;
}
else {
const infected_finish = $(`[name=infected_finish]`)[0].value;
const exposed_finish = $(`[name=exposed_finish]`)[0].value;
const [hours_infected, minutes_infected] = infected_finish.split(":").map(Number);
const elapsed_time_infected = hours_infected * 60 + minutes_infected;
const [hours_exposed, minutes_exposed] = exposed_finish.split(":").map(Number);
const elapsed_time_exposed = hours_exposed * 60 + minutes_exposed;
const max_presence_time = Math.max(elapsed_time_infected, elapsed_time_exposed);
const max_transition_time = parsedValue[parsedValue.length - 1] * 60;
if (max_transition_time < max_presence_time) {
insertErrorFor(
$("#DIVCO2_fitting_result"),
`The last transition time (${parsedValue[parsedValue.length - 1]}) should be after the last presence time (${max_presence_time / 60}).<br />`
);
submit = false;
}
}
}
else {
insertErrorFor(
$("#DIVCO2_fitting_result"),
`'${element.name}' must be a list.</br>`
@ -325,11 +355,15 @@ function submitFittingAlgorithm(url) {
.then((response) => response.json())
.then((json_response) => {
displayFittingData(json_response);
// Hide the suggestion transition lines warning
$("#suggestion_lines_txt").hide();
});
}
}
function clearFittingResultComponent() {
// Add the warning suggestion line
$("#suggestion_lines_txt").show();
// Remove all the previously generated fitting elements
$("#generate_fitting_data").prop("disabled", true);
$("#CO2_fitting_result").val("");

View file

@ -200,13 +200,13 @@
<div>
<input type="radio" id="no_ventilation" name="ventilation_type" value="no_ventilation" checked>
<label for="no_ventilation">No ventilation</label>
<input class="ml-2" type="radio" id="mechanical_ventilation" name="ventilation_type" value="mechanical_ventilation" data-enables="#DIVmechanical_ventilation">
<input class="ml-3" type="radio" id="mechanical_ventilation" name="ventilation_type" value="mechanical_ventilation" data-enables="#DIVmechanical_ventilation">
<label for="mechanical_ventilation">Mechanical</label>
</div>
<div>
<div class="ml-0">
<input type="radio" id="natural_ventilation" name="ventilation_type" value="natural_ventilation" data-enables="#DIVnatural_ventilation" data-toggle="modal" data-target="#warning_modal">
<label for="natural_ventilation">Natural</label>
<input class="ml-2" type="radio" id="from_fitting" name="ventilation_type" value="from_fitting" data-enables="#DIVfrom_fitting">
<input class="ml-3" type="radio" id="from_fitting" name="ventilation_type" value="from_fitting" data-enables="#DIVfrom_fitting">
<label for="from_fitting">Fitting</label>
</div>
</div>
@ -339,6 +339,7 @@
<div id="DIVCO2_fitting_to_submit" style="display: none">
<img id="CO2_data_plot"/><br>
<p id="suggestion_lines_txt" class="text-danger mb-3 text-center">The dashed lines are suggestions for the ventilation transition times (Pelt algorithm).</p>
<strong>Ventilation scheme:</strong>
<div>
<input class="ml-2" type="radio" id="fitting_natural_ventilation" name="fitting_ventilation_type" value='fitting_natural_ventilation' checked="checked" data-enables="#DIVfitting_natural_ventilation" form="not-submitted" >