Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 9 additions & 7 deletions doc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,15 @@
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Generating the manual website using Sphinx")

add_custom_command(
TARGET manual POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_BINARY_DIR}/doc/api/html
${SPHINX_EXTRA_API}
COMMENT "Copying generated Doxygen HTML to Sphinx extra_html/api"
)
if(DOXYGEN_FOUND)
add_custom_command(
TARGET manual POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_BINARY_DIR}/api/html
${SPHINX_BUILD}/extra_html/api
COMMENT "Copying generated Doxygen HTML to Sphinx extra_html/api"
)
endif()

install(DIRECTORY ${SPHINX_BUILD}/
DESTINATION share/codac_website
Expand Down
4 changes: 3 additions & 1 deletion doc/manual/manual/functions/analytic/analytic_functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This page provides an overview of the ``AnalyticFunction`` class, its key featur
For defining a contractor based on an ``AnalyticFunction``, the ``CtcInverse`` class is available.
It allows to solve constraints under the form :math:`\mathbf{f}(\mathbf{x})\in[\mathbf{y}]`.

See more.
:ref:`See more <sec-ctc-analytic-ctcinverse>`.


Variables
Expand Down Expand Up @@ -226,6 +226,8 @@ Evaluations can be done by the ``.eval()`` method of ``AnalyticFunction``.

For multivariate functions, it is possible to provide several inputs to the ``.eval()`` method, as in the above example with ``f2`` or ``f3``. In Codac C++, the arguments may be of different types (any mixed scalar, vector, or matrix types). In Python and Matlab however, the binding does not allow any combination of inputs, but several arguments of same structural types can be provided: a list of scalar values, or a list of vector values (``Vector``, ``IntervalVector``), or a list of matrix values (``Matrix``, ``IntervalMatrix``).

Functions can also be evaluated on time-varying inputs represented by ``SlicedTube`` objects with ``f.tube_eval(x1, x2, ...)``. This produces a new tube by applying the interval evaluation to each temporal slice. For more information, see :ref:`sec-domains-tubes-slicedtube-function-eval`.


Evaluation modes
^^^^^^^^^^^^^^^^
Expand Down
8 changes: 4 additions & 4 deletions doc/manual/manual/geometry/src.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ TEST_CASE("geometry - manual")

IntervalVector p1({0,0}), p2({1,1}), p3({-1,2});
OrientationInterval c = orientation(p1,p2,p3);
// c == OrientationInterval::CLOCKWISE
// c == OrientationInterval::COUNTERCLOCKWISE
OrientationInterval d = orientation(p3,p2,p1);
// d == OrientationInterval::COUNTERCLOCKWISE
// d == OrientationInterval::CLOCKWISE
// [geometry-1-end]

CHECK(a == OrientationInterval::COLINEAR);
CHECK(b == OrientationInterval::UNKNOWN);
CHECK(c == OrientationInterval::CLOCKWISE);
CHECK(d == OrientationInterval::COUNTERCLOCKWISE);
CHECK(c == OrientationInterval::COUNTERCLOCKWISE);
CHECK(d == OrientationInterval::CLOCKWISE);
}

{
Expand Down
8 changes: 4 additions & 4 deletions doc/manual/manual/geometry/src.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ def tests_geometry_manual(test):

p1, p2, p3 = IntervalVector([0,0]), IntervalVector([1,1]), IntervalVector([-1,2])
c = orientation(p1,p2,p3)
# c == OrientationInterval.CLOCKWISE
# c == OrientationInterval.COUNTERCLOCKWISE
d = orientation(p3,p2,p1)
# d == OrientationInterval.COUNTERCLOCKWISE
# d == OrientationInterval.CLOCKWISE
# [geometry-1-end]
test.assertTrue(a == OrientationInterval.COLINEAR)
test.assertTrue(b == OrientationInterval.UNKNOWN)
test.assertTrue(c == OrientationInterval.CLOCKWISE)
test.assertTrue(d == OrientationInterval.COUNTERCLOCKWISE)
test.assertTrue(c == OrientationInterval.COUNTERCLOCKWISE)
test.assertTrue(d == OrientationInterval.CLOCKWISE)

# [geometry-2-beg]
a = aligned([0,0],[1,1],[10,10])
Expand Down
38 changes: 38 additions & 0 deletions doc/manual/manual/tubes/slicedtube.rst
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,44 @@ For vector-valued codomains, pointwise arithmetic is also available. In particul
:dedent: 4


.. _sec-domains-tubes-slicedtube-function-eval:

Evaluating analytic functions on tubes
--------------------------------------

An :class:`~codac.AnalyticFunction` can be evaluated on one or more sliced tubes using ``f.tube_eval(x1, x2, ...)``. The result is a new tube computed by applying the function to the codomains of each temporal slice:

.. math::

[y]_k = [f]([x_1]_k, [x_2]_k, \ldots, [x_n]_k).

The result encloses :math:`f(x_1(t),\ldots,x_n(t))` at every time :math:`t`.
It shares the same ``TDomain`` as the input tubes, including its gates. All input tubes must share the same ``TDomain`` object, not merely have identical time bounds or discretizations. If the function is scalar-valued, ``tube_eval()`` returns a scalar tube. If it is vector-valued, ``tube_eval()`` returns a vector tube. In Python, the number of input tubes supported by ``tube_eval()`` is currently limited. All input tubes must be of the same kind (scalar or vector), whereas C++ also supports mixed types.

In the following example, a two-argument function maps heading and speed tubes to a 2D velocity tube :math:`(v_x,v_y)=(v\cos\theta,v\sin\theta)`. A second function computes its squared Euclidean norm :math:`v_x^2+v_y^2`, returning a scalar tube:

.. tabs::

.. group-tab:: Python

.. literalinclude:: src.py
:language: py
:start-after: [slicedtube-class-function-eval-beg]
:end-before: [slicedtube-class-function-eval-end]
:dedent: 4

.. group-tab:: C++

.. literalinclude:: src.cpp
:language: c++
:start-after: [slicedtube-class-function-eval-beg]
:end-before: [slicedtube-class-function-eval-end]
:dedent: 4

Unlike ``x(t)``, which evaluates a tube at a given time, ``f.tube_eval(x)`` evaluates an analytic function on an entire tube. It does not implicitly pass time to ``f``: if time is an argument, it must be supplied as an input tube.
For functions of time alone, ``SlicedTube(tdomain, f)`` directly evaluates ``f(t)`` over the temporal slices (see `Creating sliced tubes`_).


Integration and primitives
--------------------------

Expand Down
21 changes: 21 additions & 0 deletions doc/manual/manual/tubes/src.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,27 @@ TEST_CASE("SlicedTube class - manual")
// [slicedtube-class-7-end]
}

{
// [slicedtube-class-function-eval-beg]
auto td = create_tdomain({0,5}, 0.1, true);

ScalarVar t;
SlicedTube heading(td, AnalyticFunction({t}, 0.2*sin(t)));
SlicedTube speed(td, Interval(1,2));

ScalarVar h, v;
AnalyticFunction f({h,v}, {v*cos(h), v*sin(h)});
auto velocity = f.tube_eval(heading,speed); // SlicedTube<IntervalVector>

VectorVar p(2);
AnalyticFunction g({p}, sqr(p[0])+sqr(p[1]));
auto velocity_norm_squared = g.tube_eval(velocity); // scalar tube: ||velocity||^2
// [slicedtube-class-function-eval-end]

CHECK(velocity.size() == 2);
CHECK(velocity_norm_squared.size() == 1);
}

{
auto td = create_tdomain({0,2});
SlicedTube x(td, Interval());
Expand Down
19 changes: 19 additions & 0 deletions doc/manual/manual/tubes/src.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,25 @@ def tests_SlicedTube_manual(test):
y = A * b
# [slicedtube-class-7-end]

# [slicedtube-class-function-eval-beg]
td = create_tdomain([0,5], 0.1, True)

t = ScalarVar()
heading = SlicedTube(td, AnalyticFunction([t], 0.2*sin(t)))
speed = SlicedTube(td, Interval(1,2))

h, v = ScalarVar(), ScalarVar()
f = AnalyticFunction([h,v], [v*cos(h), v*sin(h)])
velocity = f.tube_eval(heading,speed) # vector-valued tube

p = VectorVar(2)
g = AnalyticFunction([p], sqr(p[0])+sqr(p[1]))
velocity_norm_squared = g.tube_eval(velocity) # scalar tube: ||velocity||^2
# [slicedtube-class-function-eval-end]

test.assertTrue(velocity.size() == 2)
test.assertTrue(velocity_norm_squared.size() == 1)

td = create_tdomain([0,2])
x = SlicedTube(td, Interval())

Expand Down
88 changes: 48 additions & 40 deletions python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,6 @@
endif()
# Adds pybind11::headers, pybind11::module, pybind11::embed

# The Python interpreter, which this directory, tests/ and examples/ run
# through PYTHON_EXECUTABLE: to generate the docstrings below, to build the
# wheel, and to run the Python tests and examples. pybind11 sets it where this
# directory sees it only with PYBIND11_FINDPYTHON=OFF, through
# FindPythonLibsNew.cmake. By default (COMPAT, or NEW) it uses FindPython,
# which sets Python_EXECUTABLE (or Python3_EXECUTABLE), and sets
# PYTHON_EXECUTABLE, in COMPAT, only within its own directory: here it stayed
# empty, execute_process() ran doxygen2docstring.py as a program, and the
# configuration stopped on "permission denied". It is initialized here with
# the interpreter pybind11 builds the modules for, unless it was given, and
# cached, so that tests/ and examples/, configured after this directory, get
# it too.
if(NOT PYTHON_EXECUTABLE)
if(Python_EXECUTABLE)
set(_codac_python_executable "${Python_EXECUTABLE}")
elseif(Python3_EXECUTABLE)
set(_codac_python_executable "${Python3_EXECUTABLE}")
else()
find_package(Python COMPONENTS Interpreter REQUIRED)
set(_codac_python_executable "${Python_EXECUTABLE}")
endif()
set(PYTHON_EXECUTABLE "${_codac_python_executable}" CACHE FILEPATH "Python interpreter" FORCE)
endif()
message(STATUS "Python interpreter: ${PYTHON_EXECUTABLE}")

set(PYTHON_PACKAGE_NAME ${PROJECT_NAME})
set(PYTHON_PACKAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/python_package")

Expand Down Expand Up @@ -83,18 +58,42 @@

set(NO_DOCSTRING_UPDATE OFF CACHE BOOL "Disables the update of docstrings")

file(REMOVE_RECURSE ${CMAKE_CURRENT_BINARY_DIR}/docstring)
# Regenerate docstrings without changing unchanged files' timestamps.
set(_docstring_dir "${CMAKE_CURRENT_BINARY_DIR}/docstring")
set(_docstring_tmp "${CMAKE_CURRENT_BINARY_DIR}/docstring_tmp")
file(REMOVE_RECURSE "${_docstring_tmp}")
file(MAKE_DIRECTORY "${_docstring_dir}" "${_docstring_tmp}")
message(STATUS "Generating docstrings from doxygen files...")
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/docstring)
execute_process(COMMAND ${PYTHON_EXECUTABLE}
${PROJECT_SOURCE_DIR}/scripts/doxygen/doxygen2docstring.py
${CMAKE_CURRENT_BINARY_DIR}/../doc/api/xml/
${CMAKE_CURRENT_BINARY_DIR}/docstring
RESULT_VARIABLE DOCSTRING_RESULT)
if(NOT DOCSTRING_RESULT EQUAL 0)
message(FATAL_ERROR "doxygen2docstring.py failed (exit code ${DOCSTRING_RESULT}) while generating the Python docstring headers.")
"${_docstring_tmp}"
RESULT_VARIABLE _docstring_status)
if(NOT _docstring_status EQUAL 0)
message(FATAL_ERROR "Failed to generate Python docstrings (exit code ${_docstring_status})")
endif()

file(GLOB _new_docstrings "${_docstring_tmp}/*")
foreach(_file IN LISTS _new_docstrings)
get_filename_component(_filename "${_file}" NAME)
execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_file}" "${_docstring_dir}/${_filename}"
RESULT_VARIABLE _copy_status)
if(NOT _copy_status EQUAL 0)
message(FATAL_ERROR "Failed to update Python docstring: ${_filename}")
endif()
endforeach()

# Remove headers that the generator no longer produces.
file(GLOB _old_docstrings "${_docstring_dir}/*")
foreach(_file IN LISTS _old_docstrings)
get_filename_component(_filename "${_file}" NAME)
if(NOT EXISTS "${_docstring_tmp}/${_filename}")
file(REMOVE "${_file}")
endif()
endforeach()
file(REMOVE_RECURSE "${_docstring_tmp}")

endif()


Expand Down Expand Up @@ -124,14 +123,23 @@
"${CMAKE_CURRENT_SOURCE_DIR}/../tests/codac2_tests_*.py"
)

foreach(TEST_FILE ${TESTS_FILES})
string(REPLACE "codac2_tests_" "test_" TEST_FILE_UNITTEST ${TEST_FILE}) # unittest convention
get_filename_component(TEST_FILE_UNITTEST ${TEST_FILE_UNITTEST} NAME_WE)
configure_file(${TEST_FILE} ${PYTHON_PACKAGE_DIR}/${PYTHON_PACKAGE_NAME}/tests/${TEST_FILE_UNITTEST}.py COPYONLY)
#get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE)
#set(TEST_NAME "python_${TEST_NAME}")
#add_test(NAME ${TEST_NAME} COMMAND ${PYTHON_EXECUTABLE} ${TEST_FILE})
#set_tests_properties(${TEST_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${PYTHON_PACKAGE_DIR}")
# Copy Python tests at build time instead of reconfiguring CMake
# whenever an individual test file changes.
set(_python_test_outputs)
foreach(TEST_FILE IN LISTS TESTS_FILES)
string(REPLACE "codac2_tests_" "test_" TEST_FILE_UNITTEST "${TEST_FILE}") # unittest convention
get_filename_component(TEST_FILE_UNITTEST "${TEST_FILE_UNITTEST}" NAME_WE)
set(_test_output "${PYTHON_PACKAGE_DIR}/${PYTHON_PACKAGE_NAME}/tests/${TEST_FILE_UNITTEST}.py")
add_custom_command(
OUTPUT "${_test_output}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${PYTHON_PACKAGE_DIR}/${PYTHON_PACKAGE_NAME}/tests"
COMMAND ${CMAKE_COMMAND} -E copy "${TEST_FILE}" "${_test_output}"
DEPENDS "${TEST_FILE}"
VERBATIM
)
list(APPEND _python_test_outputs "${_test_output}")
endforeach()
add_custom_target(codac_python_test_files ALL DEPENDS ${_python_test_outputs})
add_dependencies(pip_package codac_python_test_files)

endif()
endif()
11 changes: 10 additions & 1 deletion python/src/core/geometry/codac2_py_geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,16 @@ void export_geometry(py::module& m)
T_ROTATE_CONST_T_REF_CONST_INTERVAL_REF_CONST_INTERVALVECTOR_REF,
"x"_a, "a"_a, "c"_a = Vector::zero(2));

m.def("merge_adjacent_points", &merge_adjacent_points,
m.def("merge_adjacent_points",
[](py::list l)
{
auto points = l.cast<list<IntervalVector>>();
merge_adjacent_points(points);

l.attr("clear")();
for(const auto& p : points)
l.append(p);
},
VOID_MERGE_ADJACENT_POINTS_LIST_INTERVALVECTOR_REF,
"l"_a);
}
Loading
Loading