From edc409136230aa7ba92b9b6fbfff832500f2be57 Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Mon, 4 May 2026 12:26:39 +0200 Subject: [PATCH 01/16] Re-add the DE auralization method --- app/services/simulation_service.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index 17ca354..5601fbd 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -448,6 +448,13 @@ def run_solver(simulation_run_id: int, json_path: str): # the idea is to have one shared pipeline across all # methods. match simulation_method: + case "DE": + # TODO: This function is not a general auralization function and should be renamed + imp_tot, fs = auralization_calculation( + None, + json_path.replace(".json", "_pressure.csv"), + json_path.replace(".json", ".wav"), + ) case "DG": imp_tot, fs = auralization_calculation_DG( None, From af92d8c6480fbbbd674751b71b0c9ec61a90adce Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Mon, 4 May 2026 12:27:14 +0200 Subject: [PATCH 02/16] Add temporary general wav file writing functionality for all new methods returning a RIR --- app/services/simulation_service.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index 5601fbd..e3dc1d9 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -463,12 +463,19 @@ def run_solver(simulation_run_id: int, json_path: str): ) # this should be the only thing getting executed case _: - imp_tot, fs = auralization_calculation( - None, - json_path.replace(".json", "_pressure.csv"), - json_path.replace(".json", ".wav"), - ) - + # TODO: instead of reading the rir from the _pressure.csv file, read it from the json file directly + import numpy as np + imp_tot = np.loadtxt(json_path.replace(".json", "_pressure.csv"), delimiter=",") + with open(json_path, "r") as json_file: + input_data = json.load(json_file) + fs = input_data["simulationSettings"]["sampling_rate"] + rir_wav_file_name = json_path.replace(".json", ".wav") + + import pyfar as pf + rir = pf.Signal(imp_tot, fs) + pf.io.write_audio(rir, rir_wav_file_name) + logger.info(f"Impulse response shape: {imp_tot.shape}, sampling rate: {fs}") + # auralization: save the impulse response to xlsx if not ExportHelper.write_data_to_xlsx_file( From 6c21402d768bbd5084e5e3875790d69afa67bbb3 Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Mon, 4 May 2026 12:32:49 +0200 Subject: [PATCH 03/16] Add initial draft of the mono-aural auralization method --- app/services/auralization_service.py | 46 +++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/app/services/auralization_service.py b/app/services/auralization_service.py index 252128a..09e6705 100644 --- a/app/services/auralization_service.py +++ b/app/services/auralization_service.py @@ -288,15 +288,25 @@ def run_auralization(auralizationId: int) -> None: logger.debug("run auralization calculation") - #TODO: fix behavior for DG auralization, DG method output format - # should be changed. We want a single universal auralization method, - # without having to switch logic between them for each simulation method. match simulation.simulationMethod: case "DE": _, _ = auralization_calculation(signal_file_name, pressure_file_name, wav_output_file_name) case "DG": _, _ = auralization_calculation_DG(signal_file_name, pressure_file_name, wav_output_file_name) - + case _: + #TODO: We want a single universal auralization method, + # without having to switch logic between them for each simulation method. + # This will be implemented in the function mono_aural_auralization, which will be a + # general convolution-based auralization method using the RIR. + # This method does not rely on the pressure.csv file, but the wav file directly + pressure_file_name_wav = os.path.join( + DefaultConfig.UPLOAD_FOLDER_NAME, export.name.replace(".xlsx", ".wav") + ) + mono_aural_auralization( + signal_file_name, + pressure_file_name_wav, + wav_output_file_name + ) auralization.status = Status.Completed @@ -310,6 +320,34 @@ def run_auralization(auralizationId: int) -> None: abort(400, "Error running this auralization") +def mono_aural_auralization( + signal_file_name: str, + impulse_response_file_name_wav: str, + wav_output_file_name: str, + ) -> None: + """Create a mono-aural auralization by convolution. + + If the sampling rates do not match, the impulse response is resampled to + match the sampling rate of the dry input signal. + + Parameters + ---------- + signal_file_name : str + The dry input signal file name (wav format). + impulse_response_file_name_wav : str + The impulse response file name (wav format). + wav_output_file_name : str + The convolved output signal file name (wav format). + """ + + import pyfar as pf + dry_signal = pf.io.read_audio(signal_file_name) + rir = pf.io.read_audio(impulse_response_file_name_wav) + rir_resampled = pf.dsp.resample(rir, dry_signal.sampling_rate) + convolved_signal = pf.dsp.convolve(rir_resampled, dry_signal) + pf.io.write_audio(convolved_signal, wav_output_file_name) + + # TODO: too long code, refactor this function def auralization_calculation_DG( signal_file_name: Optional[str], impulse_response: str, wav_output_file_name: Optional[str] = None From 4d1c19f59d5481f3a149c680ddbba75982c71f98 Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Mon, 4 May 2026 12:34:16 +0200 Subject: [PATCH 04/16] Add pyfar as dependency (used in the newly added generalized auralization method and result export in the SimulationService and AuralizationService) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d1727ab..c38f0c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -156,4 +156,4 @@ paramiko git+https://github.com/Building-acoustics-TU-Eindhoven/acousticDE.git@d32afb2498e27bd996fc7356d57dc4f1ed76aa44#egg=acousticDE # git+https://github.com/dtu-act/deeponet-acoustic-wave-prop.git@3d3fc5ee952756eedcd4fec3c3674ad829825c7e#egg=deeponet-acoustics git+https://github.com/Building-acoustics-TU-Eindhoven/edg-acoustics.git@08cac98da98ed14ba1366741b1c0644001503b82#egg=edg-acoustics - +pyfar From 9cfc19ca70e8cd170acb9e469fd98b86eadf6a0f Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Mon, 4 May 2026 17:22:07 +0200 Subject: [PATCH 05/16] Added functionality to check whether we're running locally or from inside a docker container. Now the local version also works (for debugging the CHORAS backend). --- app/services/executors/local_executor.py | 93 ++++++++++++++++++++---- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/app/services/executors/local_executor.py b/app/services/executors/local_executor.py index 588642e..20d9e8a 100644 --- a/app/services/executors/local_executor.py +++ b/app/services/executors/local_executor.py @@ -12,40 +12,101 @@ logger = logging.getLogger(__name__) +def _is_running_in_container() -> bool: + """Return True when the current process appears to be running inside a container.""" + if os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv"): + return True + + cgroup_paths = ["/proc/self/cgroup", "/proc/1/cgroup"] + keywords = ("docker", "containerd", "kubepods", "podman", "lxc") + + for path in cgroup_paths: + if not os.path.exists(path): + continue + try: + with open(path, "r", encoding="utf-8", errors="ignore") as handle: + contents = handle.read() + except OSError: + continue + if any(keyword in contents for keyword in keywords): + return True + + return False + + +def _get_current_container(client): + """Return the Docker container object for the current container.""" + import socket + + hostname = socket.gethostname() + try: + return client.containers.get(hostname) + except docker.errors.NotFound: + for container in client.containers.list(all=True): + if hostname == container.name or hostname in container.id: + return container + raise + + def get_host_path_for_container_path(container_path: str) -> str: """ Resolves the host path corresponding to a given container path by inspecting the current container's mounts using the Docker socket. + For local debugging (not in a container), returns the container_path as-is. + Args: container_path (str): The absolute path inside the container to resolve. Returns: - str: The corresponding absolute path on the host machine. + str: The corresponding absolute path on the host machine (or container_path if local). Raises: - RuntimeError: If no mount is found covering the given container path. - Exception: If there is an error communicating with Docker or resolving the path. + RuntimeError: If no mount is found covering the given container path (in container only). + Exception: If there is an error communicating with Docker or resolving the path (in container only). """ - + if not _is_running_in_container(): + logger.warning( + f"Running locally (not in container). Returning container_path as-is: {container_path}" + ) + return container_path + try: client = docker.from_env() - import socket - hostname = socket.gethostname() - container = client.containers.get(hostname) - for mount in container.attrs["Mounts"]: - - destination = mount.get("Destination", "") - if destination == container_path: - host_source = mount["Source"] - relative = os.path.relpath(container_path, destination) - return os.path.join(host_source, relative).replace("\\", "/") + container = _get_current_container(client) + container_path = os.path.normpath(container_path) + + best_mount = None + best_destination = None + for mount in container.attrs.get("Mounts", []): + destination = mount.get("Destination") + if not destination: + continue + destination = os.path.normpath(destination) + if ( + container_path == destination + or destination == os.sep + or container_path.startswith(destination + os.sep) + ): + if best_destination is None or len(destination) > len(best_destination): + best_mount = mount + best_destination = destination + + if best_mount is None: + raise RuntimeError( + f"No mount found covering container path: {container_path}" + ) + + host_source = best_mount["Source"] + relative = os.path.relpath(container_path, best_destination) + if relative == ".": + return host_source.replace("\\", "/") + return os.path.join(host_source, relative).replace("\\", "/") + except Exception as e: logger.error(f"Could not resolve host path for {container_path}: {e}") raise - raise RuntimeError(f"No mount found covering container path: {container_path}") - class LocalExecutor(SimulationExecutor): def __init__(self, work_dir=None): From 5f87fd7fbd087dd0f185c8d5eb1f9294c503060d Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Mon, 4 May 2026 17:23:29 +0200 Subject: [PATCH 06/16] Making sure that containers are removed after using them. Both for cleaning up, as well as preventing errors with existing containers. --- app/services/simulation_service.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index e3dc1d9..9ec6c1e 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -414,16 +414,23 @@ def run_solver(simulation_run_id: int, json_path: str): "task_id": result_container["task_id"] } - logger.info(f"{simulation_method} Simulation_service:...container has been spinned up.") - container = executor.execute(method_config, sim_config) - container.wait() - logger.info(f"{simulation_method} Simulation_service:...container has finished.") + logger.info(f"{simulation_method} Simulation_service:...container has been spun up.") - cancel_flag_path = Path(json_path).parent / f"{result_container['task_id']}.cancel" + try: + container = executor.execute(method_config, sim_config) + container.wait() + logger.info(f"{simulation_method} Simulation_service:...container has finished.") + container.remove() # Clean up the container after execution + except Exception as ex: + logger.error(f"Error during container execution: {ex}") + container.remove() # Ensure container is removed even if execution fails + raise Exception(f"Error during container execution: {ex}") # logs = container.logs().decode("utf-8") # logger.info(f"{simulation_method} container FULL logs:\n{logs}") + cancel_flag_path = Path(json_path).parent / f"{result_container['task_id']}.cancel" + if os.path.exists(cancel_flag_path): logger.info("Cancelled: do not save to xlsx") else: From 2fdaebd85e31b2dfe36c07541198593483809a25 Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Mon, 4 May 2026 17:27:47 +0200 Subject: [PATCH 07/16] WIP: general auralization. DG now gets auralized using the Pyfar approach. Also refactored a bit so that a not-working xlsx export will not break the auralization. --- app/services/simulation_service.py | 139 ++++++++++++++++------------- requirements.txt | 2 +- 2 files changed, 80 insertions(+), 61 deletions(-) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index 9ec6c1e..5b111a6 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -290,6 +290,7 @@ def start_solver_task(simulation_id): "geo_path": geo_path, "results": results_container, "task_id": -1, + "fs_auralization": 44100 }, indent=4, ) @@ -426,6 +427,49 @@ def run_solver(simulation_run_id: int, json_path: str): container.remove() # Ensure container is removed even if execution fails raise Exception(f"Error during container execution: {ex}") + # auralization: generate impulse response wav file + # TODO: fix DG method such that this auralization works, + # the idea is to have one shared pipeline across all + # methods. + match simulation_method: + case "DE": + # TODO: This function is not a general auralization function and should be renamed + imp_tot, fs = auralization_calculation( + None, + json_path.replace(".json", "_pressure.csv"), + json_path.replace(".json", ".wav"), + ) + + # this should be the only thing getting executed + case _: + import numpy as np + + with open(json_path, "r") as json_file: + result_container = json.load(json_file) + + imp_tot = np.array(result_container["results"][0]["responses"][0]["receiverResults"]) + + with open(json_path, "r") as json_file: + input_data = json.load(json_file) + if "sampling_rate" in input_data["simulationSettings"]: + fs = input_data["simulationSettings"]["sampling_rate"] + else: + fs = input_data["fs_auralization"] # 44100 by default + + rir_wav_file_name = json_path.replace(".json", ".wav") + + import pyfar as pf + if imp_tot is None or len(imp_tot) == 0: + logger.warning("Impulse response data is empty or missing") + imp_tot = np.zeros(44100) # 1 second of silence at 44.1 kHz + norm_rir = pf.Signal(imp_tot, fs) + else: + rir = pf.Signal(imp_tot, fs) + norm_rir = pf.dsp.normalize(rir) + + pf.io.write_audio(norm_rir, rir_wav_file_name) + logger.info(f"Impulse response shape: {imp_tot.shape}, sampling rate: {fs}") + # logs = container.logs().decode("utf-8") # logger.info(f"{simulation_method} container FULL logs:\n{logs}") @@ -433,68 +477,43 @@ def run_solver(simulation_run_id: int, json_path: str): if os.path.exists(cancel_flag_path): logger.info("Cancelled: do not save to xlsx") + # Remove the cancel flag file after checking + try: + cancel_flag_path.unlink() + logger.info(f"Removed cancel flag file: {cancel_flag_path}") + except Exception as ex: + logger.warning(f"Failed to remove cancel flag file {cancel_flag_path}: {ex}") else: - logger.info("Saving to xlsx...") - - # save the simulation result json to xlsx - if not ExportHelper.parse_json_file_to_xlsx_file( - json_path, json_path.replace(".json", ".xlsx") - ): - logger.error("Error saving the result to xlsx") - raise "Error saving the result to xlsx" - - # db - save the xlsx file path - export = Export( - name=Path(json_path).name.replace(".json", ".xlsx"), - simulationId=simulation.id, - ) - session.add(export) - - # auralization: generate impulse response wav file - # TODO: fix DG method such that this auralization works, - # the idea is to have one shared pipeline across all - # methods. - match simulation_method: - case "DE": - # TODO: This function is not a general auralization function and should be renamed - imp_tot, fs = auralization_calculation( - None, - json_path.replace(".json", "_pressure.csv"), - json_path.replace(".json", ".wav"), - ) - case "DG": - imp_tot, fs = auralization_calculation_DG( - None, - json_path.replace(".json", "_pressure.csv"), - json_path.replace(".json", ".wav"), - ) - # this should be the only thing getting executed - case _: - # TODO: instead of reading the rir from the _pressure.csv file, read it from the json file directly - import numpy as np - imp_tot = np.loadtxt(json_path.replace(".json", "_pressure.csv"), delimiter=",") - with open(json_path, "r") as json_file: - input_data = json.load(json_file) - fs = input_data["simulationSettings"]["sampling_rate"] - rir_wav_file_name = json_path.replace(".json", ".wav") - - import pyfar as pf - rir = pf.Signal(imp_tot, fs) - pf.io.write_audio(rir, rir_wav_file_name) - logger.info(f"Impulse response shape: {imp_tot.shape}, sampling rate: {fs}") - - - # auralization: save the impulse response to xlsx - if not ExportHelper.write_data_to_xlsx_file( - json_path.replace(".json", ".xlsx"), - CustomExportParametersConfig.impulse_response, - {f"{fs}Hz": imp_tot}, - ): - logger.error( - "Error saving the impulse response to xlsx" + try: + logger.info("Saving to xlsx...") + + # save the simulation result json to xlsx + if not ExportHelper.parse_json_file_to_xlsx_file( + json_path, json_path.replace(".json", ".xlsx") + ): + logger.error("Error saving the result to xlsx") + raise "Error saving the result to xlsx" + + # db - save the xlsx file path + export = Export( + name=Path(json_path).name.replace(".json", ".xlsx"), + simulationId=simulation.id, ) - raise "Error saving the impulse response to xlsx" - + session.add(export) + + # auralization: save the impulse response to xlsx + if not ExportHelper.write_data_to_xlsx_file( + json_path.replace(".json", ".xlsx"), + CustomExportParametersConfig.impulse_response, + {f"{fs}Hz": imp_tot}, + ): + logger.error( + "Error saving the impulse response to xlsx" + ) + raise "Error saving the impulse response to xlsx" + except Exception as ex: + logger.error(f"Error during saving results: {ex}") + raise Exception(f"Error during saving results: {ex}") result_container = {} if json_path is not None: diff --git a/requirements.txt b/requirements.txt index c38f0c8..ff9e433 100644 --- a/requirements.txt +++ b/requirements.txt @@ -156,4 +156,4 @@ paramiko git+https://github.com/Building-acoustics-TU-Eindhoven/acousticDE.git@d32afb2498e27bd996fc7356d57dc4f1ed76aa44#egg=acousticDE # git+https://github.com/dtu-act/deeponet-acoustic-wave-prop.git@3d3fc5ee952756eedcd4fec3c3674ad829825c7e#egg=deeponet-acoustics git+https://github.com/Building-acoustics-TU-Eindhoven/edg-acoustics.git@08cac98da98ed14ba1366741b1c0644001503b82#egg=edg-acoustics -pyfar +pyfar \ No newline at end of file From e49784194a90f7f9cefeefcb90b41f32a66b9997 Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Mon, 4 May 2026 17:30:50 +0200 Subject: [PATCH 08/16] Copilot docstring fix --- tests/integration/test_local_executor_final.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_local_executor_final.py b/tests/integration/test_local_executor_final.py index e891238..2164cc4 100644 --- a/tests/integration/test_local_executor_final.py +++ b/tests/integration/test_local_executor_final.py @@ -134,16 +134,16 @@ def test_resolves_exact_mount_destination(self, mock_docker_client, container_wi import os assert os.path.normpath(result) == "/host/uploads" - """def test_resolves_subdirectory_of_mount(self, mock_docker_client, container_with_mounts): - + def test_resolves_subdirectory_of_mount(self, mock_docker_client, container_with_mounts): + """ U18 — EP-D1 Container path is a subdirectory of a mount → resolved by computing relative suffix and appending to host source. - + """ mock_docker_client.containers.get.return_value = container_with_mounts with patch("socket.gethostname", return_value="my-container-id"): result = get_host_path_for_container_path("/app/uploads/subdir") - assert result == "/host/uploads/subdir" """ + assert result == "/host/uploads/subdir" def test_raises_when_no_mount_covers_path(self, mock_docker_client, container_with_mounts): """ From 29ad43f08ee6fc493ecca27890fbb4465c448403 Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Wed, 6 May 2026 09:30:02 +0200 Subject: [PATCH 09/16] Added more documentation to state what part is used for local debugging. --- app/services/executors/local_executor.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/services/executors/local_executor.py b/app/services/executors/local_executor.py index 20d9e8a..5c07f05 100644 --- a/app/services/executors/local_executor.py +++ b/app/services/executors/local_executor.py @@ -11,9 +11,16 @@ logger = logging.getLogger(__name__) - +# def _is_running_in_container() -> bool: - """Return True when the current process appears to be running inside a container.""" + """ + Used to check if we're running in a container or locally for development, + to determine how to resolve paths for Docker mounts. + + Returns: + True when the current process appears to be running inside a container. + """ + if os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv"): return True @@ -65,6 +72,8 @@ def get_host_path_for_container_path(container_path: str) -> str: RuntimeError: If no mount is found covering the given container path (in container only). Exception: If there is an error communicating with Docker or resolving the path (in container only). """ + + # For local debugging, we assume the container_path is directly accessible on the host if not _is_running_in_container(): logger.warning( f"Running locally (not in container). Returning container_path as-is: {container_path}" From 335aa731a3114a8514ab296946a171e1f5a03b94 Mon Sep 17 00:00:00 2001 From: SilvinWillemsen <32464520+SilvinWillemsen@users.noreply.github.com> Date: Wed, 6 May 2026 09:31:47 +0200 Subject: [PATCH 10/16] Fix for container removal on the cloud (copilot) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/services/simulation_service.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index 5b111a6..4cbe5df 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -417,15 +417,21 @@ def run_solver(simulation_run_id: int, json_path: str): logger.info(f"{simulation_method} Simulation_service:...container has been spun up.") + container = None try: container = executor.execute(method_config, sim_config) container.wait() logger.info(f"{simulation_method} Simulation_service:...container has finished.") - container.remove() # Clean up the container after execution except Exception as ex: logger.error(f"Error during container execution: {ex}") - container.remove() # Ensure container is removed even if execution fails raise Exception(f"Error during container execution: {ex}") + finally: + remove_method = getattr(container, "remove", None) if container is not None else None + if callable(remove_method): + try: + remove_method() # Clean up local containers after execution + except Exception as cleanup_ex: + logger.warning(f"Failed to remove execution container: {cleanup_ex}") # auralization: generate impulse response wav file # TODO: fix DG method such that this auralization works, From 658b893cc8e8dc6d378a96cc1a33506bc0d0e201 Mon Sep 17 00:00:00 2001 From: SilvinWillemsen <32464520+SilvinWillemsen@users.noreply.github.com> Date: Wed, 6 May 2026 09:34:41 +0200 Subject: [PATCH 11/16] Removed unlinking of the cancel flag as we're still using it later. Next step is to unlink it at a later point Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app/services/simulation_service.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index 4cbe5df..766d024 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -483,12 +483,8 @@ def run_solver(simulation_run_id: int, json_path: str): if os.path.exists(cancel_flag_path): logger.info("Cancelled: do not save to xlsx") - # Remove the cancel flag file after checking - try: - cancel_flag_path.unlink() - logger.info(f"Removed cancel flag file: {cancel_flag_path}") - except Exception as ex: - logger.warning(f"Failed to remove cancel flag file {cancel_flag_path}: {ex}") + # Keep the cancel flag in place so later status checks in this + # function can still detect that the simulation was cancelled. else: try: logger.info("Saving to xlsx...") From 49347e7ed211731f3c323fb443898099ba0a17a1 Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Wed, 6 May 2026 09:38:32 +0200 Subject: [PATCH 12/16] Added cancel flag removal (at a better place) after removing it in the previous commit --- app/services/simulation_service.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index 766d024..b6637b8 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -534,6 +534,14 @@ def run_solver(simulation_run_id: int, json_path: str): simulation.status = Status.Completed simulation.completedAt = datetime.now() + if os.path.exists(cancel_flag_path): + # Clean up cancel flag after handling cancellation + try: + cancel_flag_path.unlink() + logger.info(f"Removed cancel flag file: {cancel_flag_path}") + except Exception as ex: + logger.warning(f"Failed to remove cancel flag file {cancel_flag_path}: {ex}") + simulation_run.updatedAt = datetime.now() simulation.updatedAt = datetime.now() From 2f7bdd6489f0e9e9ac195539c7017c8f69c18b92 Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Wed, 6 May 2026 09:50:04 +0200 Subject: [PATCH 13/16] Added comment on calling remove_method when cancelling --- app/services/simulation_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index b6637b8..5156b5e 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -431,6 +431,7 @@ def run_solver(simulation_run_id: int, json_path: str): try: remove_method() # Clean up local containers after execution except Exception as cleanup_ex: + # If cancelled, the container is already removed, so this exception will be thrown. logger.warning(f"Failed to remove execution container: {cleanup_ex}") # auralization: generate impulse response wav file From 64961c2e9628a918fcb5cb715527b91f98734ea3 Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Wed, 6 May 2026 09:55:37 +0200 Subject: [PATCH 14/16] Updated todo --- app/services/simulation_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index 5156b5e..2eb4d84 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -435,9 +435,9 @@ def run_solver(simulation_run_id: int, json_path: str): logger.warning(f"Failed to remove execution container: {cleanup_ex}") # auralization: generate impulse response wav file - # TODO: fix DG method such that this auralization works, - # the idea is to have one shared pipeline across all - # methods. + # TODO: move the auralization calculation to DE and write that + # to the JSON so that everything can be handled by the current + # default case and we can get rid of the match case. match simulation_method: case "DE": # TODO: This function is not a general auralization function and should be renamed From bbc8e0a685f077dfa5f0fa705e54a4f5aa7fd9b7 Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Wed, 6 May 2026 14:23:14 +0200 Subject: [PATCH 15/16] Revert "Copilot docstring fix" This reverts commit e49784194a90f7f9cefeefcb90b41f32a66b9997. --- tests/integration/test_local_executor_final.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_local_executor_final.py b/tests/integration/test_local_executor_final.py index 2164cc4..e891238 100644 --- a/tests/integration/test_local_executor_final.py +++ b/tests/integration/test_local_executor_final.py @@ -134,16 +134,16 @@ def test_resolves_exact_mount_destination(self, mock_docker_client, container_wi import os assert os.path.normpath(result) == "/host/uploads" - def test_resolves_subdirectory_of_mount(self, mock_docker_client, container_with_mounts): - """ + """def test_resolves_subdirectory_of_mount(self, mock_docker_client, container_with_mounts): + U18 — EP-D1 Container path is a subdirectory of a mount → resolved by computing relative suffix and appending to host source. - """ + mock_docker_client.containers.get.return_value = container_with_mounts with patch("socket.gethostname", return_value="my-container-id"): result = get_host_path_for_container_path("/app/uploads/subdir") - assert result == "/host/uploads/subdir" + assert result == "/host/uploads/subdir" """ def test_raises_when_no_mount_covers_path(self, mock_docker_client, container_with_mounts): """ From c48f87c1cdf13206c1cf29e57fce974a4d099f3c Mon Sep 17 00:00:00 2001 From: SilvinWillemsen Date: Wed, 6 May 2026 14:28:15 +0200 Subject: [PATCH 16/16] Added comments describing the normalisation procedure and why we don't normalise a signal filled with 0s. --- app/services/simulation_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/simulation_service.py b/app/services/simulation_service.py index 2eb4d84..ab1c62d 100644 --- a/app/services/simulation_service.py +++ b/app/services/simulation_service.py @@ -469,9 +469,10 @@ def run_solver(simulation_run_id: int, json_path: str): if imp_tot is None or len(imp_tot) == 0: logger.warning("Impulse response data is empty or missing") imp_tot = np.zeros(44100) # 1 second of silence at 44.1 kHz - norm_rir = pf.Signal(imp_tot, fs) + norm_rir = pf.Signal(imp_tot, fs) # don't use the pf.dsp.normalize function on an empty signal, as it returns NaN values. else: rir = pf.Signal(imp_tot, fs) + # Normalise the rir. Some methods return pressure values that are too high, which causes issues when writing to wav. norm_rir = pf.dsp.normalize(rir) pf.io.write_audio(norm_rir, rir_wav_file_name)