Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

abstracting plotting #278

Merged
merged 8 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/CI_Tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ jobs:
- name: Install this package
run: pip install -e '.[dev]'

- name: Install gnuplot
if: runner.os == 'Linux'
run: sudo apt-get install gnuplot

- name: Install gnuplot
if: runner.os == 'macOS'
run: brew install gnuplot

- name: Install gnuplot
if: runner.os == 'Windows'
run: choco install gnuplot

- name: Run tests and generate coverage reports
run: pytest --cov src/

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pep8_autoformat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: autopep8
uses: peter-evans/autopep8@v2
with:
args: --recursive --in-place --aggressive --aggressive --max-line-length 180 .
args: --recursive --in-place --aggressive --aggressive --max-line-length 180 --indent-size=2 .
boulderdaze marked this conversation as resolved.
Show resolved Hide resolved

- name: Check for changes
id: check-changes
Expand Down
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ Run an example. Notice that the output, in csv format, is printed to the termina
music_box -e Chapman
```

Output can be saved to a file in csv file when no `--output-format` is passed
Output can be saved to a csv file and printed to the terminal.

```
music_box -e Chapman -o output.csv
```

Output can be saved to a file as csv file when `--output-format` csv is passed
Output can be saved to a csv file and the terminal output can be suppressed by specifying the `--output-format`

```
music_box --output-format csv -e Chapman -o output.csv
Expand All @@ -51,13 +51,13 @@ Output can be saved to a file as netcdf file when `--output-format` netcdf is pa
music_box --output-format netcdf -e Chapman -o output.nc
```

Output can be saved to a file in csv file to output.csv when no output path is given but `--output-format` is csv
Output can be saved to a file in csv format when a filename is not specified. In this case a timestamped csv file is made

```
music_box --output-format csv -e Chapman
```

Output can be saved to a file in netcdf file to output.nc when no output path is given but `--output-format` is netcdf
Output can be saved to a file in netcdf format when a filename is not specified. In this case a timestamped netcdf file is made

```
music_box --output-format netcdf -e Chapman
Expand All @@ -69,10 +69,21 @@ You can also run your own configuration
music_box -c my_config.json
```

And, if you have gnuplot installed, some basic plots can be made to show some resulting concentrations
## Plotting
Some basic plots can be made to show concentrations throughout the simulation

### matplotlib

```
music_box -e Chapman -o output.csv --plot O1D
```

### gnuplot
If you want ascii plots (maybe you're running over ssh and can't view a graphical window), you can set
the plot tool to gnuplo (`--plot-tool gnuplot`) to view some output

```
music_box -e Chapman -o output.csv --color-output --plot CONC.O1D
music_box -e Chapman -o output.csv --plot O1D --plot-tool gnuplot
```

# Development and Contributing
Expand Down
1 change: 1 addition & 0 deletions src/acom_music_box/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
from .music_box import MusicBox
from .examples import Examples
from .data_output import DataOutput
from .plot_output import PlotOutput
26 changes: 9 additions & 17 deletions src/acom_music_box/data_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@ class DataOutput:
This class manages file paths, unit mappings, and data output formats based on
the provided arguments, ensuring valid paths and creating necessary directories.

Parameters
----------
df : pandas.DataFrame
The DataFrame containing the data to output.
args : argparse.Namespace
Arguments specifying output path, format, and additional options.

Attributes
----------
df : pandas.DataFrame
Expand Down Expand Up @@ -64,12 +57,12 @@ def __init__(self, df, args):
- output_format : str, optional
Format of the output file, either 'csv' or 'netcdf'. Defaults to 'csv'.
"""
self.df = df
self.df = df.copy(deep=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there reason for a Deep copy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, without the deep copy, the changes made on the dataframe inside this class are reflected elsewhere, and that's not what we want

self.args = args
self.unit_mapping = {
'ENV.temperature': 'K',
'ENV.pressure': 'Pa',
'ENV.number_density_air': 'kg -m3',
'ENV.number_density_air': 'kg m-3',
'time': 's'
}

Expand Down Expand Up @@ -110,7 +103,7 @@ def _convert_to_netcdf(self):

ds['ENV.temperature'].attrs = {'units': 'K'}
ds['ENV.pressure'].attrs = {'units': 'Pa'}
ds['ENV.number_density_air'].attrs = {'units': 'kg -m3'}
ds['ENV.number_density_air'].attrs = {'units': 'kg m-3'}
ds['time'].attrs = {'units': 's'}

ds.to_netcdf(self.args.output)
Expand Down Expand Up @@ -146,11 +139,10 @@ def output(self):
# Determine output type and call the respective method
if self.args.output_format is None or self.args.output_format == 'terminal':
self._output_terminal()
elif self.args.output_format is None or self.args.output_format == 'csv':

# Even if we are printing to the terminal, we still allow output to be written to csv if an output path is provided
if (self.args.output_format == 'csv') or (self.args.output is not None and self.args.output_format == 'terminal'):
self._output_csv()
elif self.args.output_format == 'netcdf':
self._output_netcdf()
else:
error = f"Unsupported output format: {self.args.output_format}"
logger.error(error)
raise ValueError(error)

if self.args.output_format == 'netcdf':
self._output_netcdf()
80 changes: 5 additions & 75 deletions src/acom_music_box/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
import datetime
import logging
import os
import subprocess
import tempfile
import matplotlib.pyplot as plt
import mplcursors
from acom_music_box import MusicBox, Examples, __version__, DataOutput
import sys
from acom_music_box import MusicBox, Examples, __version__, DataOutput, PlotOutput


def format_examples_help(examples):
Expand Down Expand Up @@ -97,69 +94,6 @@ def setup_logging(verbosity, color_output):
logging.basicConfig(level=log_level, handlers=[console_handler])


def plot_with_gnuplot(data, species_list):
# Prepare columns and data for plotting
columns = ['time'] + species_list
data_to_plot = data[columns]

data_csv = data_to_plot.to_csv(index=False)

try:
with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as data_file:
data_file.write(data_csv.encode())
data_file_path = data_file.name

plot_commands = ',\n\t'.join(
f"'{data_file_path}' using 1:{i+2} with lines title '{species}'" for i,
species in enumerate(species_list))

gnuplot_command = f"""
set datafile separator ",";
set terminal dumb size 120,25;
set xlabel 'Time';
set ylabel 'Value';
set title 'Time vs Species';
plot {plot_commands}
"""

subprocess.run(['gnuplot', '-e', gnuplot_command], check=True)
except FileNotFoundError:
logging.critical("gnuplot is not installed. Skipping plotting.")
except subprocess.CalledProcessError as e:
logging.error(f"Error occurred while plotting: {e}")
finally:
# Clean up the temporary file
if data_file_path:
os.remove(data_file_path)


def plot_with_matplotlib(data, species_list):
# Prepare columns and data for plotting
indexed = data.set_index('time')

fig, ax = plt.subplots()
indexed[species_list].plot(ax=ax)

ax.set(xlabel='Time [s]', ylabel='Concentration [mol m-3]', title='Time vs Species')

ax.spines[:].set_visible(False)
ax.spines['left'].set_visible(True)
ax.spines['bottom'].set_visible(True)

ax.grid(alpha=0.5)
ax.legend()

# Enable interactive data cursors with hover functionality
cursor = mplcursors.cursor(hover=True)

# Customize the annotation format
@cursor.connect("add")
def on_add(sel):
sel.annotation.set_text(f'Time: {sel.target[0]:.2f}\nConcentration: {sel.target[1]:1.2e}')

plt.show()


def main():
start = datetime.datetime.now()

Expand All @@ -180,8 +114,6 @@ def main():
else:
musicBoxConfigFile = args.config

musicBoxOutputPath = args.output

plot_species_list = args.plot.split(',') if args.plot else None

if not musicBoxConfigFile:
Expand All @@ -200,11 +132,9 @@ def main():
dataOutput = DataOutput(result, args)
dataOutput.output()

if plot_species_list:
if args.plot_tool == 'gnuplot':
plot_with_gnuplot(result, plot_species_list)
elif args.plot_tool == 'matplotlib':
plot_with_matplotlib(result, plot_species_list)
# Create an instance of PlotOutput
plotOutput = PlotOutput(result, args)
plotOutput.plot()

end = datetime.datetime.now()
logger.info(f"End time: {end}")
Expand Down
Loading
Loading