Skip to content

Commit d4b1bbf

Browse files
authored
Merge pull request #79 from csiro-coasts/timed_func
Add `emsarray.utils.timed_func`, set up loggers for plugins
2 parents 59c2cc6 + d359fac commit d4b1bbf

File tree

8 files changed

+126
-89
lines changed

8 files changed

+126
-89
lines changed

docs/releases/development.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Next release (in development)
88
rather than :class:`~matplotlib.collections.PatchCollection`
99
for significant speed improvements
1010
(:pr:`77`).
11+
* Added :func:`emsarray.utils.timed_func` for easily logging some performance metrics (:pr:`79`).

src/emsarray/cli/commands/export_geometry.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from emsarray.cli import BaseCommand, CommandException
1010
from emsarray.operations import geometry
1111
from emsarray.types import Pathish
12-
from emsarray.utils import PerfTimer
1312

1413
logger = logging.getLogger(__name__)
1514

@@ -70,9 +69,8 @@ def handle(self, options: argparse.Namespace) -> None:
7069
output_format = self.guess_format(output_path)
7170
logger.debug("Guessed output format as %r", output_format)
7271

73-
with PerfTimer() as t:
74-
count = dataset.ems.polygons.size
75-
logger.debug("Generated %d polygons in %f seconds", count, t.elapsed)
72+
count = dataset.ems.polygons[dataset.ems.mask].size
73+
logger.debug("Dataset contains %d polygons", count)
7674

7775
try:
7876
writer = format_writers[output_format]

src/emsarray/cli/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from shapely.geometry import box, shape
1717
from shapely.geometry.base import BaseGeometry
1818

19+
from emsarray.conventions._registry import entry_point_conventions
20+
1921
from .exceptions import CommandException
2022

2123
cli_logger = logging.getLogger('emsarray.cli')
@@ -210,6 +212,13 @@ def set_verbosity(level: int) -> None:
210212
elif level >= 3:
211213
level_str = 'DEBUG'
212214

215+
# Include logging handlers for all plugins
216+
entry_point_convention_modules = sorted({
217+
convention.__module__
218+
for convention in entry_point_conventions()
219+
if not convention.__module__.startswith('emsarray.')
220+
})
221+
213222
logging.captureWarnings(True)
214223
logging.config.dictConfig({
215224
'version': 1,
@@ -239,6 +248,10 @@ def set_verbosity(level: int) -> None:
239248
'emsarray.cli.errors': {
240249
'handlers': ['error'], 'level': level_str, 'propagate': False
241250
},
251+
**{
252+
module: {'handlers': ['console'], 'level': level_str}
253+
for module in entry_point_convention_modules
254+
},
242255
'py.warnings': {'handlers': ['console'], 'level': 'WARNING'},
243256
'__main__': {'handlers': ['console'], 'level': level_str},
244257
'shapely': {'handlers': [], 'level': 60},

src/emsarray/conventions/_base.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ def animate_on_figure(
746746
return animate_on_figure(figure, self, coordinate=coordinate, **kwargs)
747747

748748
@_requires_plot
749+
@utils.timed_func
749750
def make_poly_collection(
750751
self,
751752
data_array: Optional[DataArrayOrName] = None,
@@ -941,6 +942,7 @@ def mask(self) -> np.ndarray:
941942
return cast(np.ndarray, mask)
942943

943944
@cached_property
945+
@utils.timed_func
944946
def spatial_index(self) -> SpatialIndex[SpatialIndexItem[Index]]:
945947
"""
946948
A shapely :class:`strtree.STRtree` spatial index of all cells in this dataset.
@@ -970,16 +972,12 @@ def spatial_index(self) -> SpatialIndex[SpatialIndexItem[Index]]:
970972
--------
971973
:class:`.SpatialIndexItem`
972974
"""
973-
logger.info("Building spatial index...")
974-
with utils.PerfTimer() as timer:
975-
items = [
976-
(poly, SpatialIndexItem(index, self.unravel_index(index), poly))
977-
for index, poly in enumerate(self.polygons)
978-
if poly is not None
979-
]
980-
spatial_index = SpatialIndex(items)
981-
logger.debug("Build spatial index in %f seconds", timer.elapsed)
982-
return spatial_index
975+
items = [
976+
(poly, SpatialIndexItem(index, self.unravel_index(index), poly))
977+
for index, poly in enumerate(self.polygons)
978+
if poly is not None
979+
]
980+
return SpatialIndex(items)
983981

984982
def get_index_for_point(
985983
self,

src/emsarray/conventions/arakawa_c.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ def get_grid_kind_and_size(self, data_array: xr.DataArray) -> Tuple[ArakawaCGrid
279279
raise ValueError("Data array did not match any known grids")
280280

281281
@cached_property
282+
@utils.timed_func
282283
def polygons(self) -> np.ndarray:
283284
# Make an array of shape (j, i, 2) of all the nodes
284285
grid = np.stack([self.node.longitude.values, self.node.latitude.values], axis=-1)

src/emsarray/conventions/grid.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ def check_dataset(cls, dataset: xr.Dataset) -> Optional[int]:
387387
return Specificity.LOW
388388

389389
@cached_property
390+
@utils.timed_func
390391
def polygons(self) -> np.ndarray:
391392
lon_bounds = self.topology.longitude_bounds.values
392393
lat_bounds = self.topology.latitude_bounds.values
@@ -536,6 +537,7 @@ def check_dataset(cls, dataset: xr.Dataset) -> Optional[int]:
536537
return Specificity.LOW
537538

538539
@cached_property
540+
@utils.timed_func
539541
def polygons(self) -> np.ndarray:
540542
# Construct polygons from the bounds of the cells
541543
lon_bounds = self.topology.longitude_bounds.values

src/emsarray/conventions/ugrid.py

Lines changed: 64 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -579,26 +579,25 @@ def edge_node_array(self) -> np.ndarray:
579579
return self._to_index_array(
580580
self.edge_node_connectivity, self.edge_dimension)
581581

582-
logger.info("Building edge_node_array")
583-
with utils.PerfTimer() as timer:
584-
# Each edge is composed of two nodes. Each edge may be named twice,
585-
# once for each face. To de-duplicate this, edges are built up using
586-
# this dict-of-sets, where the dict index is the node with the
587-
# lower index, and the set is the node indices of the other end.
588-
low_highs: Dict[int, Set[int]] = defaultdict(set)
589-
590-
for face_index, node_pairs in self._face_and_node_pair_iter():
591-
for pair in node_pairs:
592-
low, high = sorted(pair)
593-
low_highs[low].add(high)
594-
edge_node = np.array([
595-
[low, high]
596-
for low, highs in low_highs.items()
597-
for high in highs
598-
], dtype=self.sensible_dtype)
599-
logger.debug("Built edge_node_array in %f seconds", timer.elapsed)
600-
601-
return edge_node
582+
return self.make_edge_node_array()
583+
584+
@utils.timed_func
585+
def make_edge_node_array(self) -> np.ndarray:
586+
# Each edge is composed of two nodes. Each edge may be named twice,
587+
# once for each face. To de-duplicate this, edges are built up using
588+
# this dict-of-sets, where the dict index is the node with the
589+
# lower index, and the set is the node indices of the other end.
590+
low_highs: Dict[int, Set[int]] = defaultdict(set)
591+
592+
for face_index, node_pairs in self._face_and_node_pair_iter():
593+
for pair in node_pairs:
594+
low, high = sorted(pair)
595+
low_highs[low].add(high)
596+
return np.array([
597+
[low, high]
598+
for low, highs in low_highs.items()
599+
for high in highs
600+
], dtype=self.sensible_dtype)
602601

603602
@cached_property
604603
def has_valid_edge_face_connectivity(self) -> bool:
@@ -650,26 +649,22 @@ def edge_face_array(self) -> np.ndarray:
650649
return self._to_index_array(
651650
self.edge_face_connectivity, self.edge_dimension)
652651

653-
# Access these outside of the timer below
654-
fill_value = self.sensible_fill_value
655-
face_edge = self.face_edge_array
652+
return self.make_edge_face_array()
656653

657-
# Build an edge_face_connectivity matrix
658-
logger.info("Building edge_face_array")
659-
with utils.PerfTimer() as timer:
660-
# The edge_face connectivity matrix
661-
shape = (self.edge_count, 2)
662-
filled = np.full(shape, fill_value, dtype=self.sensible_dtype)
663-
edge_face: np.ndarray = np.ma.masked_array(filled, mask=True)
654+
@utils.timed_func
655+
def make_edge_face_array(self) -> np.ndarray:
656+
# The edge_face connectivity matrix
657+
shape = (self.edge_count, 2)
658+
filled = np.full(shape, self.sensible_fill_value, dtype=self.sensible_dtype)
659+
edge_face: np.ndarray = np.ma.masked_array(filled, mask=True)
664660

665-
# The number of faces already seen for this edge
666-
edge_face_count = np.zeros(self.edge_count, dtype=self.sensible_dtype)
661+
# The number of faces already seen for this edge
662+
edge_face_count = np.zeros(self.edge_count, dtype=self.sensible_dtype)
667663

668-
for face_index, edge_indices in enumerate(face_edge):
669-
for edge_index in edge_indices.compressed():
670-
edge_face[edge_index, edge_face_count[edge_index]] = face_index
671-
edge_face_count[edge_index] += 1
672-
logger.debug("Built edge_face_array in %f seconds", timer.elapsed)
664+
for face_index, edge_indices in enumerate(self.face_edge_array):
665+
for edge_index in edge_indices.compressed():
666+
edge_face[edge_index, edge_face_count[edge_index]] = face_index
667+
edge_face_count[edge_index] += 1
673668

674669
return edge_face
675670

@@ -763,27 +758,24 @@ def face_edge_array(self) -> np.ndarray:
763758
return self._to_index_array(
764759
self.face_edge_connectivity, self.face_dimension)
765760

766-
# Access these outside of the timer below
767-
fill_value = self.sensible_fill_value
768-
edge_node = self.edge_node_array
761+
return self.make_face_edge_array()
769762

763+
@utils.timed_func
764+
def make_face_edge_array(self) -> np.ndarray:
770765
# Build a face_edge_connectivity matrix
771-
logger.info("Building face_edge_array")
772-
with utils.PerfTimer() as timer:
773-
shape = (self.face_count, self.max_node_count)
774-
filled = np.full(shape, fill_value, dtype=self.sensible_dtype)
775-
face_edge: np.ndarray = np.ma.masked_array(filled, mask=True)
776-
777-
node_pair_to_edge_index = {
778-
frozenset(edge): edge_index
779-
for edge_index, edge in enumerate(edge_node)
780-
}
781-
782-
for face_index, node_pairs in self._face_and_node_pair_iter():
783-
for column, node_pair in enumerate(node_pairs):
784-
edge_index = node_pair_to_edge_index[frozenset(node_pair)]
785-
face_edge[face_index, column] = edge_index
786-
logger.debug("Built face_edge_array in %f seconds", timer.elapsed)
766+
shape = (self.face_count, self.max_node_count)
767+
filled = np.full(shape, self.sensible_fill_value, dtype=self.sensible_dtype)
768+
face_edge: np.ndarray = np.ma.masked_array(filled, mask=True)
769+
770+
node_pair_to_edge_index = {
771+
frozenset(edge): edge_index
772+
for edge_index, edge in enumerate(self.edge_node_array)
773+
}
774+
775+
for face_index, node_pairs in self._face_and_node_pair_iter():
776+
for column, node_pair in enumerate(node_pairs):
777+
edge_index = node_pair_to_edge_index[frozenset(node_pair)]
778+
face_edge[face_index, column] = edge_index
787779

788780
return face_edge
789781

@@ -834,27 +826,23 @@ def face_face_array(self) -> np.ndarray:
834826
return self._to_index_array(
835827
self.face_face_connectivity, self.face_dimension)
836828

837-
# Access these outside of the timer below
838-
fill_value = self.sensible_fill_value
839-
edge_face = self.edge_face_array
829+
return self.make_face_face_array()
840830

831+
def make_face_face_array(self) -> np.ndarray:
841832
# Build a face_face_connectivity matrix
842-
logger.info("Building face_face_array")
843-
with utils.PerfTimer() as timer:
844-
face_count = np.zeros(self.face_count, dtype=self.sensible_dtype)
845-
shape = (self.face_count, self.max_node_count)
846-
filled = np.full(shape, fill_value, dtype=self.sensible_dtype)
847-
face_face: np.ndarray = np.ma.masked_array(filled, mask=True)
848-
849-
for edge_index, face_indices in enumerate(edge_face):
850-
if np.any(np.ma.getmask(face_indices)):
851-
continue
852-
left, right = face_indices
853-
face_face[left, face_count[left]] = right
854-
face_face[right, face_count[right]] = left
855-
face_count[left] += 1
856-
face_count[right] += 1
857-
logger.debug("Built face_face_array in %f seconds", timer.elapsed)
833+
face_count = np.zeros(self.face_count, dtype=self.sensible_dtype)
834+
shape = (self.face_count, self.max_node_count)
835+
filled = np.full(shape, self.sensible_fill_value, dtype=self.sensible_dtype)
836+
face_face: np.ndarray = np.ma.masked_array(filled, mask=True)
837+
838+
for edge_index, face_indices in enumerate(self.edge_face_array):
839+
if np.any(np.ma.getmask(face_indices)):
840+
continue
841+
left, right = face_indices
842+
face_face[left, face_count[left]] = right
843+
face_face[right, face_count[right]] = left
844+
face_count[left] += 1
845+
face_count[right] += 1
858846

859847
return face_face
860848

@@ -1083,6 +1071,7 @@ def get_grid_kind_and_size(
10831071
raise ValueError("Data array did not have any face, edge, or node dimension")
10841072

10851073
@cached_property
1074+
@utils.timed_func
10861075
def polygons(self) -> np.ndarray:
10871076
"""Generate list of Polygons"""
10881077
# X,Y coords of each node

src/emsarray/utils.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,41 @@ def elapsed(self) -> float:
8080
return self._stop - self._start
8181

8282

83+
def timed_func(fn: Callable[..., _T]) -> Callable[..., _T]:
84+
"""
85+
Log the execution time of the decorated function.
86+
Logs "Calling ``<func.__qualname__>``" before the wrapped function is called,
87+
and "Completed ``<func.__qualname__>`` in ``<time>``s" after.
88+
The name of the logger is taken from ``func.__module__``.
89+
90+
Example
91+
-------
92+
93+
.. code-block:: python
94+
95+
class Grass(Convention):
96+
@cached_property
97+
@timed_func
98+
def polygons(self):
99+
return ...
100+
101+
When called, this will log something like::
102+
103+
DEBUG Calling Grass.polygons
104+
DEBUG Completed Grass.polygons in 3.14s
105+
"""
106+
fn_logger = logging.getLogger(fn.__module__)
107+
108+
@functools.wraps(fn)
109+
def wrapper(*args: Any, **kwargs: Any) -> _T:
110+
fn_logger.debug("Calling %s", fn.__qualname__)
111+
with PerfTimer() as timer:
112+
value = fn(*args, **kwargs)
113+
fn_logger.debug("Completed %s in %fs", fn.__qualname__, timer.elapsed)
114+
return value
115+
return wrapper
116+
117+
83118
def to_netcdf_with_fixes(
84119
dataset: xr.Dataset,
85120
path: Pathish,

0 commit comments

Comments
 (0)