diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a59f45d..711a124 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,8 @@ on: branches: - 'master' + workflow_dispatch: + jobs: test: runs-on: ubuntu-22.04 diff --git a/README.md b/README.md index 30b346b..4f36a94 100644 --- a/README.md +++ b/README.md @@ -12,27 +12,27 @@ This tool was developed as part of a PhD, "Modelling and Design of Local Energy 1. Install [Python 3.10](https://www.python.org/downloads/) and [Git](https://git-scm.com/). 2. Clone this git repo onto your machine: -``` -git clone git@github.com:andrewlyden/PyLESA.git -``` + ``` + git clone git@github.com:andrewlyden/PyLESA.git + ``` 3. Install the `PyLESA` python virtual environment. -If using Linux: -``` -python3.10 -m venv venv -source venv/bin/activate -python -m pip install --upgrade pip -python -m pip install -r requirements.txt -``` - -If using Windows and Powershell: -``` -python -m venv venv -.\venv\Scripts\activate.ps1 -python -m pip install --upgrade pip -python -m pip install -r requirements.txt -``` + If using Linux: + ``` + python3.10 -m venv venv + source venv/bin/activate + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + ``` + + If using Windows and Powershell: + ``` + python -m venv venv + .\venv\Scripts\activate.ps1 + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + ``` 4. Define and gather data on the local energy system to be modelled including resources, demands, supply, storage, grid connection, and control strategy. Define the increments and ranges to be modelled within the required parametric design. Input all this data using one of the template Excel Workbooks from the [inputs](./inputs) folder. @@ -40,26 +40,33 @@ python -m pip install -r requirements.txt 6. Using a terminal (e.g. PowerShell) within the clone of the `PyLESA` git repo, run: -If using Linux: + If using Linux: -```python -source venv/bin/activate -python -m pylesa --help # to display help messages -python -m pylesa ./inputs/{name of Excel input file}.xlsx my/existing/output/directory --overwrite -``` + ```python + source venv/bin/activate + python -m pylesa --help # to display help messages + python -m pylesa ./inputs/{name of Excel input file}.xlsx my/existing/output/directory --overwrite + ``` -If using Windows and Powershell: + If using Windows and Powershell: -```python -.\venv\Scripts\activate.ps1 -python -m pylesa --help # to display help messages -python -m pylesa ./inputs/{name of Excel input file}.xlsx my/existing/output/directory --overwrite -``` + ```python + .\venv\Scripts\activate.ps1 + python -m pylesa --help # to display help messages + python -m pylesa ./inputs/{name of Excel input file}.xlsx my/existing/output/directory --overwrite + ``` + + Running `python -m pylesa --help` will display the following help message: + ![pylesa usage](./img/pylesa_usage.png) + + Note that PyLESA defaults to using 2 compute cores: 1 to run the solver, 1 to generate matplotlib + figures and write them to disc. Using the `--singlecore` command-line option will force PyLESA + to run on a single core which will increase the overall runtime. -Running `python -m pylesa --help` will display the following help message: -![pylesa usage](./img/pylesa_usage.png) +7. After the run is complete, open the outpus folder in your chosen run directory to view the KPI 3D plots and/or operational graphs, as well as .csv outputs (note that an error will be raised if only one simulation combination is run, as 3D plots cannot be processed). There are also raw outputs.pkl files for each simulation combination which contains a vast range of raw outputs. -7. After the run is complete, open the outpus folder in your chosen run directory to view the KPI 3D plots and/or operational graphs, as well as .csv outputs. (Note an error will be raised if only one simulation combination is run, as 3D plots cannot be processed). There are also raw outputs.pkl files for each simulation combination which contains a vast range of raw outputs. + Information about the run is written to a `pylesa.log` file located in the output folder. This + file contains details of run progress and any warning or error messages that may have occurred. A video discussing how to run `PyLESA` is available here: https://youtu.be/QsJut9ftCT4 diff --git a/img/pylesa_usage.png b/img/pylesa_usage.png index 7b58323..e56b317 100644 Binary files a/img/pylesa_usage.png and b/img/pylesa_usage.png differ diff --git a/pylesa/__init__.py b/pylesa/__init__.py index 36415f4..e69de29 100644 --- a/pylesa/__init__.py +++ b/pylesa/__init__.py @@ -1,4 +0,0 @@ -from .constants import DEFAULT_LOGLEVEL -from .logging import setup_logging - -setup_logging(DEFAULT_LOGLEVEL) \ No newline at end of file diff --git a/pylesa/constants.py b/pylesa/constants.py index f2539ec..a84def1 100644 --- a/pylesa/constants.py +++ b/pylesa/constants.py @@ -1,9 +1,9 @@ import logging -DEFAULT_LOGLEVEL = logging.INFO +DEFAULT_LOGLEVEL = logging.WARNING FILE_LOG_FORMAT = "%(asctime)s: %(levelname)s: %(message)s" CONSOLE_LOG_FORMAT = "%(levelname)s: %(message)s" -LOG_PATH = "pylesa.log" +LOG_FILENAME = "pylesa.log" INDIR = "inputs" OUTDIR = "outputs" diff --git a/pylesa/controllers/fixed_order.py b/pylesa/controllers/fixed_order.py index d188ba8..d8d3f23 100644 --- a/pylesa/controllers/fixed_order.py +++ b/pylesa/controllers/fixed_order.py @@ -12,8 +12,7 @@ from pathlib import Path import numpy as np import pickle - -from progressbar import Bar, ETA, Percentage, ProgressBar, RotatingMarker +from tqdm import tqdm from .. import initialise_classes from ..io import inputs @@ -93,13 +92,6 @@ def run_timesteps(self, first_hour, timesteps): soc_list = [] results = [] - # includes a progress bar, purely for aesthetics - widgets = ['Running: ' + self.subname, ' ', Percentage(), ' ', - Bar(marker=RotatingMarker(), left='[', right=']'), - ' ', ETA()] - pbar = ProgressBar(widgets=widgets, maxval=timesteps) - pbar.start() - # can run number of timesteps up to 8760 # final hour is from first hour plus number of timesteps final_hour = first_hour + timesteps @@ -109,7 +101,11 @@ def run_timesteps(self, first_hour, timesteps): raise ValueError(msg) # run controller for each timestep - for timestep in range(first_hour, final_hour): + for timestep in tqdm( + range(first_hour, final_hour), + desc=f"Solving: {self.subname}", + leave=False + ): heat_demand = self.heat_demand[timestep] source_temp = self.source_temp[timestep] flow_temp = self.flow_temp[timestep] @@ -161,11 +157,6 @@ def run_timesteps(self, first_hour, timesteps): soc_list.append( run['ES']['final_soc']) - # update for progress bar - pbar.update(timestep - first_hour + 1) - # stop progress bar - pbar.finish() - # write the outputs to a pickle file = self.root / OUTDIR / self.subname / 'outputs.pkl' with open(file, 'wb') as output_file: diff --git a/pylesa/controllers/mpc.py b/pylesa/controllers/mpc.py index fb37bda..223f781 100644 --- a/pylesa/controllers/mpc.py +++ b/pylesa/controllers/mpc.py @@ -6,8 +6,8 @@ from gekko import GEKKO from pathlib import Path import numpy as np -from progressbar import Bar, ETA, Percentage, ProgressBar, RotatingMarker import pickle +from tqdm import tqdm from .. import initialise_classes, tools from ..io import inputs @@ -548,13 +548,6 @@ def moving_horizon(self, pre_calc, first_hour, timesteps): horizon = self.horizon final_hour = first_hour + timesteps - # includes a progress bar, purely for aesthetics - widgets = ['Running: ' + self.subname, ' ', Percentage(), ' ', - Bar(marker=RotatingMarker(), left='[', right=']'), - ' ', ETA()] - pbar = ProgressBar(widgets=widgets, maxval=timesteps) - pbar.start() - if final_hour > 8760: msg = f'The final timestep is {final_hour} which is beyond the end of the year (8760)' LOG.error(msg) @@ -562,7 +555,11 @@ def moving_horizon(self, pre_calc, first_hour, timesteps): results = [] next_results = [] - for hour in range(first_hour, final_hour - 1): + for hour in tqdm( + range(first_hour, final_hour - 1), + desc=f"Solving: {self.subname}", + leave=False + ): final_horizon_hour = hour + horizon if final_horizon_hour > 8759: final_horizon_hour = 8759 @@ -669,11 +666,6 @@ def moving_horizon(self, pre_calc, first_hour, timesteps): results.append(r['results']) next_results.append(r['next_results']) - # update for progress bar - pbar.update(hour - first_hour + 1) - # stop progress bar - pbar.finish() - # write the outputs to a pickle file = self.root / OUTDIR / self.subname / 'outputs.pkl' with open(file, 'wb') as output_file: diff --git a/pylesa/io/outputs.py b/pylesa/io/outputs.py index a040a40..28b3803 100644 --- a/pylesa/io/outputs.py +++ b/pylesa/io/outputs.py @@ -7,6 +7,8 @@ # import register_matplotlib_converters import matplotlib import matplotlib.pyplot as plt +import time +from tqdm import tqdm from typing import Dict from . import inputs @@ -54,6 +56,7 @@ def run_plots(root: str | Path, subname: str): + then = time.time() root = Path(root).resolve() myInputs = inputs.Inputs(root, subname) # controller inputs @@ -82,20 +85,26 @@ def run_plots(root: str | Path, subname: str): myPlots.grid() myPlots.RES_bar() + LOG.info(f"Written output files for: {subname}. Time taken: {int(round(time.time() - then,0))} seconds") def run_KPIs(root: str | Path): root = Path(root).resolve() + jobs = [] + my3DPlots = ThreeDPlots(root) - my3DPlots.KPIs_to_csv() - my3DPlots.plot_opex() - my3DPlots.plot_RES() - my3DPlots.plot_heat_from_RES() - my3DPlots.plot_HP_size_ratio() - my3DPlots.plot_HP_utilisation() - my3DPlots.plot_capital_cost() - my3DPlots.plot_LCOH() - my3DPlots.plot_COH() + jobs.append(my3DPlots.KPIs_to_csv) + jobs.append(my3DPlots.plot_opex) + jobs.append(my3DPlots.plot_RES) + jobs.append(my3DPlots.plot_heat_from_RES) + jobs.append(my3DPlots.plot_HP_size_ratio) + jobs.append(my3DPlots.plot_HP_utilisation) + jobs.append(my3DPlots.plot_capital_cost) + jobs.append(my3DPlots.plot_LCOH) + jobs.append(my3DPlots.plot_COH) + + for job in tqdm(jobs, desc=f"Writing KPIs"): + job() class Plot(object): diff --git a/pylesa/io/read_excel.py b/pylesa/io/read_excel.py index d227d3e..5c24c9c 100644 --- a/pylesa/io/read_excel.py +++ b/pylesa/io/read_excel.py @@ -22,7 +22,7 @@ def read_inputs(xlsxpath: str | Path, root: Path) -> None: root: path to directory to store intermediary inputs and outputs """ xlsxpath = valid_fpath(xlsxpath) - LOG.info(f'Reading MS Excel file: {xlsxpath.name}') + LOG.info(f'Reading MS Excel file: {xlsxpath}') root = Path(root).resolve() myInput = XlsxInput(xlsxpath, root) diff --git a/pylesa/logging.py b/pylesa/logging.py index a42ee74..4c8b7c9 100644 --- a/pylesa/logging.py +++ b/pylesa/logging.py @@ -1,7 +1,8 @@ import enum import logging +from pathlib import Path -from .constants import LOG_PATH, CONSOLE_LOG_FORMAT, FILE_LOG_FORMAT +from .constants import LOG_FILENAME, CONSOLE_LOG_FORMAT, FILE_LOG_FORMAT class CustomFormatter(logging.Formatter): @@ -26,21 +27,23 @@ def format(self, record): return formatter.format(record) -def setup_logging(level: enum.Enum): +def setup_logging(logdir: str | Path, level: enum.Enum): """Sets up logging handlers, removes any existing handlers A FileHandler and a StreamHandler (console) are created. Args: + logdir: path to directory to write log file level: Enum defining the logging level """ root = logging.getLogger() - root.setLevel(level) + root.setLevel(min(logging.INFO, level)) root.handlers.clear() handlers = [] # Log to a file on disk - file_handler = logging.FileHandler(LOG_PATH, "w+") + # Level always set at least to INFO + file_handler = logging.FileHandler(Path(logdir).resolve() / LOG_FILENAME, "w+") file_handler.setFormatter(logging.Formatter(FILE_LOG_FORMAT)) handlers.append(file_handler) diff --git a/pylesa/main.py b/pylesa/main.py index 5589af0..3cf4c0f 100644 --- a/pylesa/main.py +++ b/pylesa/main.py @@ -2,22 +2,56 @@ from pathlib import Path import shutil import time +from tqdm import tqdm from . import parametric_analysis +from .constants import DEFAULT_LOGLEVEL from .controllers import fixed_order from .controllers import mpc +from .logging import setup_logging from .io import inputs, outputs, read_excel from .io.paths import valid_dir, valid_fpath +from .mp.process import OutputProcess LOG = logging.getLogger(__name__) -def main(xlsxpath: str, outdir: str, overwrite: bool = False): +def run_solver(controller: str, subname: str, outdir: Path, first_hour: int, timesteps: int): + then = time.time() + if controller == 'Fixed order control': + fixed_order.FixedOrder( + outdir, subname).run_timesteps( + first_hour, timesteps) + LOG.info(f'Ran fixed order controller: {subname}. Time taken: {int(round(time.time() - then, 0))} seconds') + + elif controller == 'Model predictive control': + myScheduler = mpc.Scheduler( + outdir, subname) + pre_calc = myScheduler.pre_calculation( + first_hour, timesteps) + myScheduler.moving_horizon( + pre_calc, first_hour, timesteps) + LOG.info(f'Ran predictive controller: {subname}. Time taken: {int(round(time.time() - then, 0))} seconds') + + else: + msg = f'Invalid controller chosen: {controller}' + LOG.error(msg) + raise ValueError(msg) + +def main(xlsxpath: str, outdir: str, overwrite: bool = False, singlecore: bool = False): """Run PyLESA, an open source tool capable of modelling local energy systems. - Args: + By default, this function runs the PyLESA solver in the main process but + outsources the writing of matplotlib files to a second process. The writing + of matplotlib files takes roughly the same amount of time as the Fixed Order + solver takes, so there is a significant performance benefit to running + multiple cores. Multithreading is not an option since the artist functions + in matplotlib are not necessarily thread safe.\n\n + + Args:\n xlsxpath: path to Excel input file\n outdir: path to output directory, a sub-directory matching the Excel filename will be created\n - overwrite: bool flag to overwrite existing output, default: False + overwrite: bool flag to overwrite existing output, default: False\n + singlecore: bool flag to run on a single core rather than two cores, default: False (uses two cores) """ xlsxpath = valid_fpath(xlsxpath) outdir = valid_dir(outdir) / xlsxpath.stem @@ -35,13 +69,15 @@ def main(xlsxpath: str, outdir: str, overwrite: bool = False): raise FileExistsError(msg) outdir.mkdir() + # Setup logging to console / file + setup_logging(outdir, DEFAULT_LOGLEVEL) + t0 = time.time() # generate pickle inputs from excel sheet read_excel.read_inputs(xlsxpath, outdir) # generate pickle inputs for parametric analysis - LOG.info('Generating inputs for all simuation combinations...') myPara = parametric_analysis.Para(outdir) myPara.create_pickles() combinations = myPara.folder_name @@ -50,7 +86,8 @@ def main(xlsxpath: str, outdir: str, overwrite: bool = False): t1 = time.time() tot_time = (t1 - t0) - LOG.info(f'Input complete. Time taken: {round(tot_time, 2)} seconds.') + LOG.info(f'Input complete. Time taken: {int(round(tot_time, 0))} seconds') + LOG.info(f"Running {num_combos} combinations of heat pump power / storage size") # just take first subname as controller is same in all subname = myPara.folder_name[0] @@ -62,43 +99,48 @@ def main(xlsxpath: str, outdir: str, overwrite: bool = False): timesteps = controller_info['total_timesteps'] first_hour = controller_info['first_hour'] - # run controller and outputs for all combinations - for i in range(num_combos): - - # combo to be run - subname = combinations[i] - - if controller == 'Fixed order control': - # run fixed order controller - LOG.info(f'Running fixed order controller: {subname}') - fixed_order.FixedOrder( - outdir, subname).run_timesteps( - first_hour, timesteps) - - elif controller == 'Model predictive control': - # run mpc controller - LOG.info('Running model predictive controller...') - myScheduler = mpc.Scheduler( - outdir, subname) - pre_calc = myScheduler.pre_calculation( - first_hour, timesteps) - myScheduler.moving_horizon( - pre_calc, first_hour, timesteps) - - else: - msg = f'Unsuitable controller chosen: {controller}' - LOG.error(msg) - raise ValueError(msg) - - LOG.info('Running output analysis...') - # run output analysis - outputs.run_plots(outdir, subname) + if singlecore: + LOG.info("Running pylesa using a single compute core.") + # Single core + for i in tqdm(range(num_combos), desc="Jobs"): + # combo to be run + subname = combinations[i] + run_solver(controller, subname, outdir, first_hour, timesteps) + # Run output + outputs.run_plots(outdir, subname) + else: + LOG.info("Running pylesa using 2 compute cores.") + # Run two processes: + # - main process runs the solver + # - second process runs the output (to produce matplotlib figures) + p = OutputProcess() + + try: + # Start output process + # Process waits to write output until a job is submitted + p.start(outputs.run_plots) + + # Run controller for all combinations + for i in tqdm(range(num_combos), desc="Jobs"): + # combo to be run + subname = combinations[i] + run_solver(controller, subname, outdir, first_hour, timesteps) + # Submit job to output queue for writing + p.submit([outdir, subname]) + + except Exception as e: + p.cancel() + raise e + + finally: + # Stop output process once job is complete + p.stop() tx = time.time() outputs.run_KPIs(outdir) ty = time.time() tot_time = (ty - tx) - LOG.info(f'Time taken for running KPIs: {round(tot_time, 2)} seconds') + LOG.info(f'Wrote KPIs. Time taken: {int(round(tot_time, 0))} seconds') t2 = time.time() tot_time = (t2 - t1) / 60 diff --git a/pylesa/mp/__init__.py b/pylesa/mp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pylesa/mp/constants.py b/pylesa/mp/constants.py new file mode 100644 index 0000000..aff5af5 --- /dev/null +++ b/pylesa/mp/constants.py @@ -0,0 +1,4 @@ +"""Constants associated with multi-processing""" + +SENTINEL = "STOP_JOB" +TIMEOUT = 30.0 diff --git a/pylesa/mp/logging.py b/pylesa/mp/logging.py new file mode 100644 index 0000000..07b1fa3 --- /dev/null +++ b/pylesa/mp/logging.py @@ -0,0 +1,15 @@ +import enum +import logging +import logging.handlers +from multiprocessing import Queue + + +def setup_mp_logging(level: enum.Enum, queue: Queue): + """Sets up StreamHandler to write to queue""" + root = logging.getLogger().root + root.handlers.clear() + root.setLevel(min(logging.INFO, level)) + + # Log to the queue + queue_handler = logging.handlers.QueueHandler(queue) + root.addHandler(queue_handler) diff --git a/pylesa/mp/process.py b/pylesa/mp/process.py new file mode 100644 index 0000000..3784c4a --- /dev/null +++ b/pylesa/mp/process.py @@ -0,0 +1,95 @@ +"""Run jobs in separate process""" + +import logging.handlers +from multiprocessing import Process, Queue +import logging +from threading import Thread +from typing import Callable, List, Any + +from .constants import SENTINEL, TIMEOUT +from .logging import setup_mp_logging +from ..constants import DEFAULT_LOGLEVEL + +LOG = logging.getLogger(__name__) + + +class OutputProcess: + """Run func on a separate process submitting jobs via a queue""" + + def __init__(self): + self._job_queue = Queue() + self._log_queue = Queue() + self._process = None + self._logger = None + + @staticmethod + def _run_job(func: Callable, job_queue: Queue, log_queue: Queue): + # setup logging to pass messages back to main process + root_logger = logging.getLogger().root + setup_mp_logging(root_logger.level, log_queue) + # Run job loop + try: + while True: + job = job_queue.get() + if job == SENTINEL: + break + func(*job) + except Exception as e: + LOG.error(e) + raise e + + def start(self, func: Callable) -> None: + # Ensure queues are empty + self._job_queue.empty() + self._log_queue.empty() + # Create queue listener to forward messages to existing handlers + self._logger = logging.handlers.QueueListener( + self._log_queue, *logging.getLogger().handlers, respect_handler_level=True + ) + self._logger.start() + # Start process + self._process = Process( + target=self._run_job, args=(func, self._job_queue, self._log_queue) + ) + self._process.start() + + def submit(self, args: List[Any]): + self._job_queue.put(args) + + def stop(self, block=True, timeout=TIMEOUT): + try: + if self._process: + self._job_queue.put(SENTINEL) + if not block: + timeout = None + + # Wait for process to join + if self.is_alive(): + self._process.join(timeout=timeout) + + if self._process.exitcode != 0: + msg = f"Output process exited with non-zero exit code: {self._process.exitcode}" + LOG.error(msg) + raise SystemError(msg) + finally: + if self._process: + self._process.close() + self._job_queue.empty() + self._process = None + + if self._logger: + self._logger.stop() + self._logger = None + + def cancel(self, block=True): + self._job_queue.empty() + self.stop(block) + + def is_alive(self): + try: + if self._process: + return self._process.is_alive() + else: + return False + except ValueError: + return False diff --git a/requirements.txt b/requirements.txt index 65575c2..0759812 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,6 @@ pathspec==0.12.1 pillow==10.3.0 platformdirs==4.2.2 pluggy==1.5.0 -progressbar33==2.4 pvlib==0.10.4 pycodestyle==2.11.1 pyflakes==3.2.0 @@ -50,6 +49,7 @@ snakeviz==2.2.0 threadpoolctl==3.5.0 tomli==2.0.1 tornado==6.4 +tqdm==4.66.4 tuna==0.5.11 typer==0.12.3 typing_extensions==4.11.0 diff --git a/tests/test_process.py b/tests/test_process.py new file mode 100644 index 0000000..c6a9f20 --- /dev/null +++ b/tests/test_process.py @@ -0,0 +1,91 @@ +import logging +import io +from pathlib import Path +import pytest +import time + +from pylesa.mp.process import OutputProcess + +LOG = logging.getLogger(__name__) + + +def task(filepath: str): + filepath = Path(filepath).resolve() + with open(filepath, "w") as f: + f.write("test") + LOG.info(f"Wrote: {filepath.stem}") + + +@pytest.fixture +def process(): + p = OutputProcess() + p.start(task) + yield p + p.stop() + + +class TestOutputProcess: + def test_run_job(self, process: OutputProcess, tmpdir): + fpaths = [Path(tmpdir) / f"test_{idx}.txt" for idx in range(5)] + for fpath in fpaths: + process.submit([fpath]) + + # Wait for files to be written + time.sleep(2) + + for fpath in fpaths: + assert Path(fpath).exists() + + def test_handle_task_error(self, process: OutputProcess): + bad_path = "bad/path" + with pytest.raises(SystemError): + process.submit([bad_path]) + # Stop waits for jobs to finish and queue to be emptied + process.stop() + + assert not process.is_alive() + + def test_cancel(self, process: OutputProcess): + process.cancel() + assert process._process is None + assert process._logger is None + + def test_restart(self, process: OutputProcess): + process.cancel() + assert process._process is None + process.start(task) + assert process.is_alive() + + +class TestLogging: + @pytest.fixture + def stream_handler(self): + # Add in string io object + root = logging.getLogger() + stream = io.StringIO() + root.addHandler(logging.StreamHandler(stream=stream)) + yield stream + + @pytest.fixture + def process_and_stream(self, stream_handler): + """Setup process after setting up stream_handler""" + p = OutputProcess() + p.start(task) + yield p, stream_handler + p.stop() + + def test_logging(self, process_and_stream, tmpdir): + process, stream = process_and_stream + + fpaths = [Path(tmpdir) / f"test_{idx}.txt" for idx in range(5)] + for fpath in fpaths: + process.submit([fpath]) + + time.sleep(2) + + # Check log messages + stream.seek(0) + lines = stream.readlines() + assert len(lines) == len(fpaths) + for fpath in fpaths: + assert f"Wrote: {fpath.stem}\n" in lines diff --git a/tests/test_pylesa.py b/tests/test_pylesa.py index e7f436a..bb42c71 100644 --- a/tests/test_pylesa.py +++ b/tests/test_pylesa.py @@ -15,23 +15,37 @@ def fixed_order_input(): @pytest.fixture -def outdir(): - return Path("tests/data").resolve() +def csvpaths(): + pth = Path() + return [ + pth / "outputs" / "KPIs" / "KPI_economic_fixed_order.csv", + pth / "outputs" / "KPIs" / "KPI_technical_fixed_order.csv", + pth / "outputs" / "KPIs" / "output_fixed_order.csv", + ] @pytest.fixture -def fixed_order_paths(fixed_order_input: Path, outdir: Path) -> List[Path]: +def fixed_order_paths(fixed_order_input: Path, csvpaths) -> List[Path]: runname = fixed_order_input.stem - return [ - outdir / runname / "outputs" / "KPIs" / "KPI_economic_fixed_order.csv", - outdir / runname / "outputs" / "KPIs" / "KPI_technical_fixed_order.csv", - outdir / runname / "outputs" / "KPIs" / "output_fixed_order.csv", - ] + return [Path("tests/data").resolve() / runname / csvpth for csvpth in csvpaths] + + +@pytest.fixture +def temp_paths(fixed_order_input: Path, tmpdir: Path, csvpaths) -> List[Path]: + runname = fixed_order_input.stem + return [tmpdir / runname / csvpth for csvpth in csvpaths] class TestPylesa: - def test_regression( - self, fixed_order_input: Path, outdir: Path, fixed_order_paths: List[Path] + # Test single core and multiprocessing run options + @pytest.mark.parametrize("singlecore", [True, False]) + def test_main( + self, + fixed_order_input: Path, + fixed_order_paths: List[Path], + tmpdir: Path, + temp_paths: List[Path], + singlecore: bool ): # Load existing results, these are committed to the repo targets = [] @@ -39,11 +53,11 @@ def test_regression( targets.append(pd.read_csv(csvpath)) # Run pylesa - main(fixed_order_input, outdir, overwrite=True) + main(fixed_order_input, tmpdir, singlecore=singlecore) # Check new results - for idx, outpath in enumerate(fixed_order_paths): + for idx, outpath in enumerate(temp_paths): expected = targets[idx] got = pd.read_csv(outpath) assert expected.columns.all() == got.columns.all() - assert np.allclose(expected.values, got.values) + assert np.allclose(expected.values, got.values) \ No newline at end of file