Skip to content

Commit 47142a9

Browse files
authored
Merge pull request #385 from xylar/add_build_mesh_logging
Add support for logging (e.g. to log files)
2 parents 8979e01 + 70dbd3d commit 47142a9

File tree

5 files changed

+372
-147
lines changed

5 files changed

+372
-147
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import sys
2+
import logging
3+
import subprocess
4+
5+
6+
def check_call(args, logger):
7+
"""
8+
Call the given subprocess with logging to the given logger.
9+
10+
Parameters
11+
----------
12+
args : list
13+
A list of argument to the subprocess
14+
15+
logger : logging.Logger
16+
The logger to write output to
17+
18+
Raises
19+
------
20+
subprocess.CalledProcessError
21+
If the given subprocess exists with nonzero status
22+
23+
"""
24+
25+
process = subprocess.Popen(args, stdout=subprocess.PIPE,
26+
stderr=subprocess.PIPE)
27+
stdout, stderr = process.communicate()
28+
29+
if stdout:
30+
stdout = stdout.decode('utf-8')
31+
for line in stdout.split('\n'):
32+
logger.info(line)
33+
if stderr:
34+
stderr = stderr.decode('utf-8')
35+
for line in stderr.split('\n'):
36+
logger.error(line)
37+
38+
if process.returncode != 0:
39+
raise subprocess.CalledProcessError(process.returncode,
40+
' '.join(args))
41+
42+
43+
class LoggingContext(object):
44+
45+
"""
46+
A context manager for creating a logger or using an existing logger
47+
48+
Attributes
49+
----------
50+
logger : logging.Logger
51+
A logger that sends output to a log file or stdout/stderr
52+
"""
53+
54+
def __init__(self, name, logger=None, log_filename=None):
55+
"""
56+
If ``logger`` is ``None``, create a new logger either to a log file
57+
or stdout/stderr. If ``logger`` is anything else, just set the logger
58+
attribute
59+
60+
Parameters
61+
----------
62+
name : str
63+
A unique name for the logger (e.g. ``__name__`` of the calling
64+
module)
65+
66+
logger : logging.Logger, optional
67+
An existing logger that sends output to a log file or stdout/stderr
68+
to be used in this context
69+
70+
log_filename : str, optional
71+
The name of a file where output should be written. If none is
72+
supplied, output goes to stdout/stderr
73+
"""
74+
self.logger = logger
75+
self.name = name
76+
self.log_filename = log_filename
77+
self.handler = None
78+
self.old_stdout = None
79+
self.old_stderr = None
80+
self.existing_logger = logger is not None
81+
82+
def __enter__(self):
83+
if not self.existing_logger:
84+
if self.log_filename is not None:
85+
# redirect output to a log file
86+
logger = logging.getLogger(self.name)
87+
handler = logging.FileHandler(self.log_filename)
88+
else:
89+
logger = logging.getLogger(self.name)
90+
handler = logging.StreamHandler(sys.stdout)
91+
92+
formatter = MpasFormatter()
93+
handler.setFormatter(formatter)
94+
logger.addHandler(handler)
95+
logger.setLevel(logging.INFO)
96+
logger.propagate = False
97+
self.logger = logger
98+
self.handler = handler
99+
100+
if self.log_filename is not None:
101+
self.old_stdout = sys.stdout
102+
self.old_stderr = sys.stderr
103+
sys.stdout = StreamToLogger(logger, logging.INFO)
104+
sys.stderr = StreamToLogger(logger, logging.ERROR)
105+
return self.logger
106+
107+
def __exit__(self, exc_type, exc_val, exc_tb):
108+
if not self.existing_logger:
109+
if self.old_stdout is not None:
110+
self.handler.close()
111+
# restore stdout and stderr
112+
sys.stdout = self.old_stdout
113+
sys.stderr = self.old_stderr
114+
115+
# remove the handlers from the logger (probably only necessary if
116+
# writeLogFile==False)
117+
self.logger.handlers = []
118+
119+
self.stdout = self.original_stdout = sys.stdout
120+
self.stderr = self.original_stderr = sys.stderr
121+
122+
123+
class MpasFormatter(logging.Formatter):
124+
"""
125+
A custom formatter for logging
126+
Modified from:
127+
https://stackoverflow.com/a/8349076/7728169
128+
https://stackoverflow.com/a/14859558/7728169
129+
"""
130+
# Authors
131+
# -------
132+
# Xylar Asay-Davis
133+
134+
# printing error messages without a prefix because they are sometimes
135+
# errors and sometimes only warnings sent to stderr
136+
dbg_fmt = "DEBUG: %(module)s: %(lineno)d: %(msg)s"
137+
info_fmt = "%(msg)s"
138+
err_fmt = info_fmt
139+
140+
def __init__(self, fmt=info_fmt):
141+
logging.Formatter.__init__(self, fmt)
142+
143+
def format(self, record):
144+
145+
# Save the original format configured by the user
146+
# when the logger formatter was instantiated
147+
format_orig = self._fmt
148+
149+
# Replace the original format with one customized by logging level
150+
if record.levelno == logging.DEBUG:
151+
self._fmt = MpasFormatter.dbg_fmt
152+
153+
elif record.levelno == logging.INFO:
154+
self._fmt = MpasFormatter.info_fmt
155+
156+
elif record.levelno == logging.ERROR:
157+
self._fmt = MpasFormatter.err_fmt
158+
159+
# Call the original formatter class to do the grunt work
160+
result = logging.Formatter.format(self, record)
161+
162+
# Restore the original format configured by the user
163+
self._fmt = format_orig
164+
165+
return result
166+
167+
168+
class StreamToLogger(object):
169+
"""
170+
Modified based on code by:
171+
https://www.electricmonk.nl/log/2011/08/14/redirect-stdout-and-stderr-to-a-logger-in-python/
172+
Copyright (C) 2011 Ferry Boender
173+
License: GPL, see https://www.electricmonk.nl/log/posting-license/
174+
Fake file-like stream object that redirects writes to a logger instance.
175+
"""
176+
177+
def __init__(self, logger, log_level=logging.INFO):
178+
self.logger = logger
179+
self.log_level = log_level
180+
self.linebuf = ''
181+
182+
def write(self, buf):
183+
for line in buf.rstrip().splitlines():
184+
self.logger.log(self.log_level, str(line.rstrip()))
185+
186+
def flush(self):
187+
pass

conda_package/mpas_tools/mesh/creation/build_mesh.py

Lines changed: 83 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
from mpas_tools.mesh.creation.jigsaw_driver import jigsaw_driver
1313
from mpas_tools.mesh.creation.jigsaw_to_netcdf import jigsaw_to_netcdf
1414
from mpas_tools.viz.colormaps import register_sci_viz_colormaps
15+
from mpas_tools.logging import LoggingContext
1516

1617

1718
def build_spherical_mesh(cellWidth, lon, lat, earth_radius,
1819
out_filename='base_mesh.nc', plot_cellWidth=True,
19-
dir='./'):
20+
dir='./', logger=None):
2021
"""
2122
Build an MPAS mesh using JIGSAW with the given cell sizes as a function of
2223
latitude and longitude.
@@ -43,64 +44,70 @@ def build_spherical_mesh(cellWidth, lon, lat, earth_radius,
4344
The file name of the resulting MPAS mesh
4445
4546
plot_cellWidth : bool, optional
46-
Whether to produce a plot of ``cellWidth``. If so, it will be written to
47-
``cellWidthGlobal.png``.
47+
Whether to produce a plot of ``cellWidth``. If so, it will be written
48+
to ``cellWidthGlobal.png``.
4849
4950
dir : str, optional
5051
A directory in which a temporary directory will be added with files
5152
produced during mesh conversion and then deleted upon completion.
53+
54+
logger : logging.Logger, optional
55+
A logger for the output if not stdout
5256
"""
5357

54-
da = xarray.DataArray(cellWidth,
55-
dims=['lat', 'lon'],
56-
coords={'lat': lat, 'lon': lon},
57-
name='cellWidth')
58-
cw_filename = 'cellWidthVsLatLon.nc'
59-
da.to_netcdf(cw_filename)
60-
if plot_cellWidth:
61-
register_sci_viz_colormaps()
62-
fig = plt.figure(figsize=[16.0, 8.0])
63-
ax = plt.axes(projection=ccrs.PlateCarree())
64-
ax.set_global()
65-
im = ax.imshow(cellWidth, origin='lower',
66-
transform=ccrs.PlateCarree(),
67-
extent=[-180, 180, -90, 90], cmap='3Wbgy5',
68-
zorder=0)
69-
ax.add_feature(cartopy.feature.LAND, edgecolor='black', zorder=1)
70-
gl = ax.gridlines(
71-
crs=ccrs.PlateCarree(),
72-
draw_labels=True,
73-
linewidth=1,
74-
color='gray',
75-
alpha=0.5,
76-
linestyle='-', zorder=2)
77-
gl.top_labels = False
78-
gl.right_labels = False
79-
plt.title(
80-
'Grid cell size, km, min: {:.1f} max: {:.1f}'.format(
81-
cellWidth.min(),cellWidth.max()))
82-
plt.colorbar(im, shrink=.60)
83-
fig.canvas.draw()
84-
plt.tight_layout()
85-
plt.savefig('cellWidthGlobal.png', bbox_inches='tight')
86-
plt.close()
87-
88-
print('Step 1. Generate mesh with JIGSAW')
89-
jigsaw_driver(cellWidth, lon, lat, on_sphere=True,
90-
earth_radius=earth_radius)
91-
92-
print('Step 2. Convert triangles from jigsaw format to netcdf')
93-
jigsaw_to_netcdf(msh_filename='mesh-MESH.msh',
94-
output_name='mesh_triangles.nc', on_sphere=True,
95-
sphere_radius=earth_radius)
96-
97-
print('Step 3. Convert from triangles to MPAS mesh')
98-
write_netcdf(convert(xarray.open_dataset('mesh_triangles.nc'), dir=dir),
99-
out_filename)
58+
with LoggingContext(__name__, logger=logger) as logger:
59+
60+
da = xarray.DataArray(cellWidth,
61+
dims=['lat', 'lon'],
62+
coords={'lat': lat, 'lon': lon},
63+
name='cellWidth')
64+
cw_filename = 'cellWidthVsLatLon.nc'
65+
da.to_netcdf(cw_filename)
66+
if plot_cellWidth:
67+
register_sci_viz_colormaps()
68+
fig = plt.figure(figsize=[16.0, 8.0])
69+
ax = plt.axes(projection=ccrs.PlateCarree())
70+
ax.set_global()
71+
im = ax.imshow(cellWidth, origin='lower',
72+
transform=ccrs.PlateCarree(),
73+
extent=[-180, 180, -90, 90], cmap='3Wbgy5',
74+
zorder=0)
75+
ax.add_feature(cartopy.feature.LAND, edgecolor='black', zorder=1)
76+
gl = ax.gridlines(
77+
crs=ccrs.PlateCarree(),
78+
draw_labels=True,
79+
linewidth=1,
80+
color='gray',
81+
alpha=0.5,
82+
linestyle='-', zorder=2)
83+
gl.top_labels = False
84+
gl.right_labels = False
85+
plt.title(
86+
'Grid cell size, km, min: {:.1f} max: {:.1f}'.format(
87+
cellWidth.min(),cellWidth.max()))
88+
plt.colorbar(im, shrink=.60)
89+
fig.canvas.draw()
90+
plt.tight_layout()
91+
plt.savefig('cellWidthGlobal.png', bbox_inches='tight')
92+
plt.close()
93+
94+
logger.info('Step 1. Generate mesh with JIGSAW')
95+
jigsaw_driver(cellWidth, lon, lat, on_sphere=True,
96+
earth_radius=earth_radius, logger=logger)
97+
98+
logger.info('Step 2. Convert triangles from jigsaw format to netcdf')
99+
jigsaw_to_netcdf(msh_filename='mesh-MESH.msh',
100+
output_name='mesh_triangles.nc', on_sphere=True,
101+
sphere_radius=earth_radius)
102+
103+
logger.info('Step 3. Convert from triangles to MPAS mesh')
104+
write_netcdf(convert(xarray.open_dataset('mesh_triangles.nc'), dir=dir,
105+
logger=logger),
106+
out_filename)
100107

101108

102109
def build_planar_mesh(cellWidth, x, y, geom_points, geom_edges,
103-
out_filename='base_mesh.nc'):
110+
out_filename='base_mesh.nc', logger=None):
104111
"""
105112
Build a planar MPAS mesh
106113
@@ -121,28 +128,31 @@ def build_planar_mesh(cellWidth, x, y, geom_points, geom_edges,
121128
122129
out_filename : str, optional
123130
The file name of the resulting MPAS mesh
131+
132+
logger : logging.Logger, optional
133+
A logger for the output if not stdout
124134
"""
125-
da = xarray.DataArray(cellWidth,
126-
dims=['y', 'x'],
127-
coords={'y': y, 'x': x},
128-
name='cellWidth')
129-
cw_filename = 'cellWidthVsXY.nc'
130-
da.to_netcdf(cw_filename)
131-
132-
print('Step 1. Generate mesh with JIGSAW')
133-
jigsaw_driver(
134-
cellWidth,
135-
x,
136-
y,
137-
on_sphere=False,
138-
geom_points=geom_points,
139-
geom_edges=geom_edges)
140-
141-
print('Step 2. Convert triangles from jigsaw format to netcdf')
142-
jigsaw_to_netcdf(msh_filename='mesh-MESH.msh',
143-
output_name='mesh_triangles.nc', on_sphere=False)
144-
145-
print('Step 3. Convert from triangles to MPAS mesh')
146-
write_netcdf(convert(xarray.open_dataset('mesh_triangles.nc')),
147-
out_filename)
135+
136+
with LoggingContext(__name__, logger=logger) as logger:
137+
138+
da = xarray.DataArray(cellWidth,
139+
dims=['y', 'x'],
140+
coords={'y': y, 'x': x},
141+
name='cellWidth')
142+
cw_filename = 'cellWidthVsXY.nc'
143+
da.to_netcdf(cw_filename)
144+
145+
logger.info('Step 1. Generate mesh with JIGSAW')
146+
jigsaw_driver(cellWidth, x, y, on_sphere=False,
147+
geom_points=geom_points, geom_edges=geom_edges,
148+
logger=logger)
149+
150+
logger.info('Step 2. Convert triangles from jigsaw format to netcdf')
151+
jigsaw_to_netcdf(msh_filename='mesh-MESH.msh',
152+
output_name='mesh_triangles.nc', on_sphere=False)
153+
154+
logger.info('Step 3. Convert from triangles to MPAS mesh')
155+
write_netcdf(convert(xarray.open_dataset('mesh_triangles.nc'),
156+
logger=logger),
157+
out_filename)
148158

0 commit comments

Comments
 (0)