From 47713e1b50b9885844ad06743b7259fa752fbd76 Mon Sep 17 00:00:00 2001 From: Howard Butler Date: Fri, 31 Jul 2026 08:39:33 -0500 Subject: [PATCH 1/3] add a an timing parameter to enable logtiming option in Pipeline invocations --- README.rst | 3 +++ src/pdal/PyPipeline.cpp | 4 ++-- src/pdal/PyPipeline.hpp | 2 +- src/pdal/StreamableExecutor.cpp | 3 ++- src/pdal/StreamableExecutor.hpp | 1 + src/pdal/libpdalpython.cpp | 12 +++++++++--- src/pdal/pipeline.py | 22 +++++++++++++++++++--- test/test_pipeline.py | 29 ++++++++++++++++++++++++++++- 8 files changed, 65 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index cb9bebbc..c9f181f8 100644 --- a/README.rst +++ b/README.rst @@ -73,6 +73,9 @@ sorts it by the ``X`` dimension: metadata = pipeline.metadata log = pipeline.log +Pass ``timing=True`` when creating a pipeline to include PDAL timing +information in ``pipeline.log`` after execution. + Programmatic Pipeline Construction ................................................................................ diff --git a/src/pdal/PyPipeline.cpp b/src/pdal/PyPipeline.cpp index 7f295273..64abe7a7 100644 --- a/src/pdal/PyPipeline.cpp +++ b/src/pdal/PyPipeline.cpp @@ -56,12 +56,12 @@ void CountPointTable::reset() PipelineExecutor::PipelineExecutor( - std::string const& json, std::vector> arrays, int level) + std::string const& json, std::vector> arrays, int level, bool timing) { if (level < 0 || level > 8) throw pdal_error("log level must be between 0 and 8!"); - LogPtr log(Log::makeLog("pypipeline", &m_logStream)); + LogPtr log(Log::makeLog("pypipeline", &m_logStream, timing)); log->setLevel(static_cast(level)); m_manager.setLog(log); diff --git a/src/pdal/PyPipeline.hpp b/src/pdal/PyPipeline.hpp index 1eed023f..45fa241e 100644 --- a/src/pdal/PyPipeline.hpp +++ b/src/pdal/PyPipeline.hpp @@ -58,7 +58,7 @@ class Array; class PDAL_EXPORT PipelineExecutor { public: - PipelineExecutor(std::string const& json, std::vector> arrays, int level); + PipelineExecutor(std::string const& json, std::vector> arrays, int level, bool timing); virtual ~PipelineExecutor() = default; point_count_t execute(pdal::StringList allowedDims); diff --git a/src/pdal/StreamableExecutor.cpp b/src/pdal/StreamableExecutor.cpp index 5fa01931..aad2d9d2 100644 --- a/src/pdal/StreamableExecutor.cpp +++ b/src/pdal/StreamableExecutor.cpp @@ -186,10 +186,11 @@ char *PythonPointTable::getPoint(PointId idx) StreamableExecutor::StreamableExecutor(std::string const& json, std::vector> arrays, int level, + bool timing, point_count_t chunkSize, int prefetch, pdal::StringList allowedDims) - : PipelineExecutor(json, arrays, level) + : PipelineExecutor(json, arrays, level, timing) , m_table(chunkSize, prefetch) , m_exc(nullptr) { diff --git a/src/pdal/StreamableExecutor.hpp b/src/pdal/StreamableExecutor.hpp index f565c8ee..bb852755 100644 --- a/src/pdal/StreamableExecutor.hpp +++ b/src/pdal/StreamableExecutor.hpp @@ -80,6 +80,7 @@ class StreamableExecutor : public PipelineExecutor StreamableExecutor(std::string const& json, std::vector> arrays, int level, + bool timing, point_count_t chunkSize, int prefetch, pdal::StringList allowedDim); diff --git a/src/pdal/libpdalpython.cpp b/src/pdal/libpdalpython.cpp index 09118fbf..9ce7a939 100644 --- a/src/pdal/libpdalpython.cpp +++ b/src/pdal/libpdalpython.cpp @@ -186,7 +186,7 @@ namespace pdal { std::unique_ptr iterator(int chunk_size, int prefetch, pdal::StringList allowedDims) { return std::unique_ptr(new PipelineIterator( - getJson(), _inputs, _loglevel, chunk_size, prefetch, allowedDims + getJson(), _inputs, _loglevel, _timing, chunk_size, prefetch, allowedDims )); } @@ -214,6 +214,10 @@ namespace pdal { void setLogLevel(int level) { _loglevel = level; delExecutor(); } + bool getTiming() { return _timing; } + + void setTiming(bool timing) { _timing = timing; delExecutor(); } + std::string getLog() { return getExecutor()->getLog(); } std::string getPipeline() { return getExecutor()->getPipeline(); } @@ -291,14 +295,15 @@ namespace pdal { // does for all of the other methods it knows about py::gil_scoped_acquire acquire; if (!_executor) - _executor.reset(new PipelineExecutor(getJson(), _inputs, _loglevel)); + _executor.reset(new PipelineExecutor(getJson(), _inputs, _loglevel, _timing)); return _executor.get(); } private: std::unique_ptr _executor; std::vector> _inputs; - int _loglevel; + int _loglevel = 0; + bool _timing = false; }; @@ -324,6 +329,7 @@ namespace pdal { .def("iterator", &Pipeline::iterator, "chunk_size"_a=10000, "prefetch"_a=0, py::arg("allowed_dims") =py::list()) .def_property("inputs", nullptr, &Pipeline::setInputs) .def_property("loglevel", &Pipeline::getLoglevel, &Pipeline::setLogLevel) + .def_property("timing", &Pipeline::getTiming, &Pipeline::setTiming) .def_property_readonly("log", &Pipeline::getLog) .def_property_readonly("schema", &Pipeline::getSchema) .def_property_readonly("srswkt2", &Pipeline::getSrsWKT2) diff --git a/src/pdal/pipeline.py b/src/pdal/pipeline.py index 4310b232..889bc075 100644 --- a/src/pdal/pipeline.py +++ b/src/pdal/pipeline.py @@ -49,6 +49,8 @@ def __init__( json: Optional[str] = None, dataframes: Sequence[DataFrame] = (), stream_handlers: Sequence[Callable[[], int]] = (), + *, + timing: bool = False, ): if json: @@ -75,6 +77,7 @@ def __init__( self.inputs = [(a, None) for a in arrays] self.loglevel = loglevel + self.timing = timing def __getstate__(self): state = self.pipeline @@ -104,6 +107,14 @@ def loglevel(self, value: int) -> None: # super() property setter is not supported libpdalpython.Pipeline.loglevel.__set__(self, loglevel) + @property + def timing(self) -> bool: + return super().timing + + @timing.setter + def timing(self, value: bool) -> None: + libpdalpython.Pipeline.timing.__set__(self, bool(value)) + def __ior__(self, other: Union[Stage, Pipeline]) -> Pipeline: if isinstance(other, Stage): self._stages.append(other) @@ -124,7 +135,7 @@ def __or__(self, other: Union[Stage, Pipeline]) -> Pipeline: return new def __copy__(self) -> Pipeline: - clone = self.__class__(loglevel=self.loglevel) + clone = self.__class__(loglevel=self.loglevel, timing=self.timing) clone._copy_inputs(self) clone |= self return clone @@ -214,8 +225,13 @@ def inputs(self) -> List[Union[Stage, str]]: def options(self) -> Dict[str, Any]: return dict(self._options) - def pipeline(self, *arrays: np.ndarray, loglevel: int = logging.ERROR) -> Pipeline: - return Pipeline((self,), arrays, loglevel) + def pipeline( + self, + *arrays: np.ndarray, + loglevel: int = logging.ERROR, + timing: bool = False, + ) -> Pipeline: + return Pipeline((self,), arrays, loglevel=loglevel, timing=timing) def __or__(self, other: Union[Stage, Pipeline]) -> Pipeline: return Pipeline((self, other)) diff --git a/test/test_pipeline.py b/test/test_pipeline.py index 9bb3ba89..3eac09e9 100644 --- a/test/test_pipeline.py +++ b/test/test_pipeline.py @@ -1,6 +1,7 @@ import json import logging import os +import re import sys from itertools import product @@ -53,6 +54,21 @@ def test_construction(self, filename): assert isinstance(p, pdal.Pipeline) assert len(p.stages) == 2 + def test_timing_is_optional_public_api(self): + with open(os.path.join(DATADIRECTORY, "chip.json"), "r") as f: + pipeline_json = f.read() + + p = pdal.Pipeline(None, (), logging.ERROR, pipeline_json) + assert p.timing is False + assert p.execute() == 1065 + + with pytest.raises(TypeError): + pdal.Pipeline(None, (), logging.ERROR, pipeline_json, (), (), True) + + reader = pdal.Reader(os.path.join(DATADIRECTORY, "1.2-with-color.las")) + assert reader.pipeline().timing is False + assert reader.pipeline(timing=True).timing is True + @pytest.mark.parametrize( "pipeline", [ @@ -338,6 +354,7 @@ def test_logging(self, filename): """Can we fetch log output""" r = get_pipeline(filename) assert r.loglevel == logging.ERROR + assert r.timing is False assert r.log == "" for loglevel in logging.CRITICAL, -1: @@ -356,6 +373,17 @@ def test_logging(self, filename): assert "(pypipeline Debug) Executing pipeline in standard mode" in r.log assert "(pypipeline writers.las Debug)" in r.log + def test_logging_timing(self): + """Can we fetch log output decorated with timing information""" + with open(os.path.join(DATADIRECTORY, "chip.json"), "r") as f: + r = pdal.Pipeline(f.read(), loglevel=logging.DEBUG, timing=True) + + assert r.timing is True + count = r.execute() + assert count == 1065 + assert re.search(r"\(pypipeline readers\.las Debug [0-9.]+\)", r.log) + assert re.search(r"\(pypipeline Debug [0-9.]+\) Executing pipeline in standard mode", r.log) + @pytest.mark.skipif( not hasattr(pdal.Filter, "python"), reason="filters.python PDAL plugin is not available", @@ -904,4 +932,3 @@ def invalid_stream_handler(): with pytest.raises(RuntimeError, match=f"Stream chunk size not in the range of array length: {invalid_chunk_size}"): p.execute() - From e28a6d374bd01bbcb57a2ac0aa9145c5730fd705 Mon Sep 17 00:00:00 2001 From: Howard Butler Date: Fri, 31 Jul 2026 08:45:11 -0500 Subject: [PATCH 2/3] add note to readme --- README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c9f181f8..e352ca4b 100644 --- a/README.rst +++ b/README.rst @@ -74,7 +74,16 @@ sorts it by the ``X`` dimension: log = pipeline.log Pass ``timing=True`` when creating a pipeline to include PDAL timing -information in ``pipeline.log`` after execution. +information in ``pipeline.log`` after execution: + +.. code-block:: python + + import logging + import pdal + + pipeline = pdal.Pipeline(json, loglevel=logging.DEBUG, timing=True) + count = pipeline.execute() + timing_log = pipeline.log Programmatic Pipeline Construction ................................................................................ From 6cf6531d77a33a143d1d11301bfdc97509821a44 Mon Sep 17 00:00:00 2001 From: Howard Butler Date: Fri, 31 Jul 2026 09:17:32 -0500 Subject: [PATCH 3/3] bump to 3.5.5 --- src/pdal/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pdal/__init__.py b/src/pdal/__init__.py index 6a9c4b35..fb790e0d 100644 --- a/src/pdal/__init__.py +++ b/src/pdal/__init__.py @@ -1,5 +1,5 @@ __all__ = ["Pipeline", "Stage", "Reader", "Filter", "Writer", "dimensions", "info"] -__version__ = '3.5.4' +__version__ = '3.5.5' from . import libpdalpython from .drivers import inject_pdal_drivers