Skip to content
Open
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
46 changes: 46 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Checks that run on every commit. They cover the fast, purely local gates:
# formatting and linting for each language in the tree, plus the Python type
# check. Tests, native builds, and backend conformance are not here; they need
# a configured build and hardware, and run through CTest and the conformance
# scripts instead.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
- id: check-toml
- id: check-merge-conflict
- id: check-added-large-files
# The device photomasks under docs/tutorials/devices are legitimately
# large and are tracked deliberately.
args: [--maxkb=1024]
exclude: ^docs/tutorials/devices/
- id: end-of-file-fixer
exclude: ^docs/tutorials/devices/
- id: trailing-whitespace
exclude: ^docs/tutorials/devices/

# Pinned to the version the project depends on, so a hook run and a local
# `ruff check` enforce the same rule set. Formatting is deliberately absent:
# the linter's rules, including import order, are what this tree follows.
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.3
hooks:
- id: ruff
args: [--fix]

- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.7
hooks:
- id: clang-format
types_or: [c++, c, cuda, objective-c++, metal]
files: ^cpp/

- repo: local
hooks:
- id: pyright
name: pyright
entry: uv run pyright
language: system
pass_filenames: false
types: [python]
6 changes: 4 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,12 @@ if(CM_ENABLE_METAL)
add_custom_command(
OUTPUT "${CM_METAL_COUPLED_RATES_HEADER}"
COMMAND "${CMAKE_COMMAND}"
"-DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/cpp/metal/kernels/coupled_rates.metal"
"-DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/cpp/metal/kernels/grid_transport.metal;${CMAKE_CURRENT_SOURCE_DIR}/cpp/metal/kernels/coupled_rates.metal"
"-DOUTPUT=${CM_METAL_COUPLED_RATES_HEADER}"
"-DSYMBOL=coupled_rates_source"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedMetalSource.cmake"
DEPENDS
cpp/metal/kernels/grid_transport.metal
cpp/metal/kernels/coupled_rates.metal
cmake/EmbedMetalSource.cmake
VERBATIM
Expand All @@ -121,11 +122,12 @@ if(CM_ENABLE_METAL)
add_custom_command(
OUTPUT "${CM_METAL_SIGNALS_HEADER}"
COMMAND "${CMAKE_COMMAND}"
"-DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/cpp/metal/kernels/signals.metal"
"-DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/cpp/metal/kernels/grid_transport.metal;${CMAKE_CURRENT_SOURCE_DIR}/cpp/metal/kernels/signals.metal"
"-DOUTPUT=${CM_METAL_SIGNALS_HEADER}"
"-DSYMBOL=signals_source"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedMetalSource.cmake"
DEPENDS
cpp/metal/kernels/grid_transport.metal
cpp/metal/kernels/signals.metal
cmake/EmbedMetalSource.cmake
VERBATIM
Expand Down
12 changes: 10 additions & 2 deletions cmake/EmbedMetalSource.cmake
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# INPUT names one Metal source, or several to concatenate in order. A Metal
# library is compiled from source at runtime with no include path, so a source
# that shares helpers with another receives them by concatenation here.
if(NOT DEFINED INPUT OR NOT DEFINED OUTPUT OR NOT DEFINED SYMBOL)
message(FATAL_ERROR "EmbedMetalSource.cmake requires INPUT, OUTPUT, and SYMBOL")
endif()
if(NOT SYMBOL MATCHES "^[A-Za-z_][A-Za-z0-9_]*$")
message(FATAL_ERROR "EmbedMetalSource.cmake received an invalid C++ symbol")
endif()

file(READ "${INPUT}" CM_METAL_SOURCE)
get_filename_component(CM_OUTPUT_DIRECTORY "${OUTPUT}" DIRECTORY)
file(MAKE_DIRECTORY "${CM_OUTPUT_DIRECTORY}")
file(WRITE "${OUTPUT}" "#pragma once\n\nnamespace cm::metal {\ninline constexpr char ${SYMBOL}[] = R\"CM_METAL(")
file(APPEND "${OUTPUT}" "${CM_METAL_SOURCE}")
foreach(CM_METAL_INPUT IN LISTS INPUT)
file(READ "${CM_METAL_INPUT}" CM_METAL_SOURCE)
if(CM_METAL_SOURCE MATCHES "CM_METAL\\(" OR CM_METAL_SOURCE MATCHES "\\)CM_METAL")
message(FATAL_ERROR "Metal source ${CM_METAL_INPUT} contains the embedding delimiter")
endif()
file(APPEND "${OUTPUT}" "${CM_METAL_SOURCE}")
endforeach()
file(APPEND "${OUTPUT}" [=[)CM_METAL";
} // namespace cm::metal
]=])
67 changes: 21 additions & 46 deletions cpp/core/constraints.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <cmath>
#include <limits>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <utility>

Expand Down Expand Up @@ -44,9 +45,8 @@ void validate_sphere(const SphereConstraint& sphere) {
}

bool positive_finite_extents(const Vec3& half_extents) {
return std::isfinite(half_extents.x) && half_extents.x > 0.0F &&
std::isfinite(half_extents.y) && half_extents.y > 0.0F &&
std::isfinite(half_extents.z) && half_extents.z > 0.0F;
return std::isfinite(half_extents.x) && half_extents.x > 0.0F && std::isfinite(half_extents.y) &&
half_extents.y > 0.0F && std::isfinite(half_extents.z) && half_extents.z > 0.0F;
}

void validate_box(const BoxConstraint& box) {
Expand Down Expand Up @@ -94,50 +94,25 @@ void validate_constraint_state(ConstraintId next_id, std::span<const PlaneConstr
}
std::unordered_set<ConstraintId> ids;
ids.reserve(total);
ConstraintId previous_plane = invalid_constraint_id;
for (const auto& plane : planes) {
validate_plane(plane);
if (plane.id <= previous_plane || plane.id >= next_id) {
throw std::invalid_argument("checkpoint plane identifiers are not ordered and allocated");
const auto check_ordered = [&ids, next_id](const auto& constraints, auto&& validate,
const char* kind) {
ConstraintId previous = invalid_constraint_id;
for (const auto& constraint : constraints) {
validate(constraint);
if (constraint.id <= previous || constraint.id >= next_id) {
throw std::invalid_argument(std::string("checkpoint ") + kind +
" identifiers are not ordered and allocated");
}
if (!ids.insert(constraint.id).second) {
throw std::invalid_argument("checkpoint contains a duplicate constraint identifier");
}
previous = constraint.id;
}
if (!ids.insert(plane.id).second) {
throw std::invalid_argument("checkpoint contains a duplicate constraint identifier");
}
previous_plane = plane.id;
}
ConstraintId previous_sphere = invalid_constraint_id;
for (const auto& sphere : spheres) {
validate_sphere(sphere);
if (sphere.id <= previous_sphere || sphere.id >= next_id) {
throw std::invalid_argument("checkpoint sphere identifiers are not ordered and allocated");
}
if (!ids.insert(sphere.id).second) {
throw std::invalid_argument("checkpoint contains a duplicate constraint identifier");
}
previous_sphere = sphere.id;
}
ConstraintId previous_box = invalid_constraint_id;
for (const auto& box : boxes) {
validate_box(box);
if (box.id <= previous_box || box.id >= next_id) {
throw std::invalid_argument("checkpoint box identifiers are not ordered and allocated");
}
if (!ids.insert(box.id).second) {
throw std::invalid_argument("checkpoint contains a duplicate constraint identifier");
}
previous_box = box.id;
}
ConstraintId previous_cylinder = invalid_constraint_id;
for (const auto& cylinder : cylinders) {
validate_cylinder(cylinder);
if (cylinder.id <= previous_cylinder || cylinder.id >= next_id) {
throw std::invalid_argument("checkpoint cylinder identifiers are not ordered and allocated");
}
if (!ids.insert(cylinder.id).second) {
throw std::invalid_argument("checkpoint contains a duplicate constraint identifier");
}
previous_cylinder = cylinder.id;
}
};
check_ordered(planes, validate_plane, "plane");
check_ordered(spheres, validate_sphere, "sphere");
check_ordered(boxes, validate_box, "box");
check_ordered(cylinders, validate_cylinder, "cylinder");
}

std::size_t checked_offset_count(std::size_t cell_count) {
Expand Down
22 changes: 11 additions & 11 deletions cpp/core/contact_graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ void validate_contact_parameters(const ContactParameters& parameters) {
}
}

std::vector<ContactCandidate> find_cell_contact_candidates(
const WorldState& state, const ContactParameters& parameters) {
std::vector<ContactCandidate> find_cell_contact_candidates(const WorldState& state,
const ContactParameters& parameters) {
validate_contact_parameters(parameters);
const auto geometry = state.geometry_state();
std::vector<CapsuleBounds> bounds;
Expand Down Expand Up @@ -112,10 +112,10 @@ std::vector<ContactCandidate> find_cell_contact_candidates(
});
active.erase(expired.begin(), expired.end());
for (const auto* candidate : active) {
const auto overlaps_y = candidate->maximum_y >= current.minimum_y &&
current.maximum_y >= candidate->minimum_y;
const auto overlaps_z = candidate->maximum_z >= current.minimum_z &&
current.maximum_z >= candidate->minimum_z;
const auto overlaps_y =
candidate->maximum_y >= current.minimum_y && current.maximum_y >= candidate->minimum_y;
const auto overlaps_z =
candidate->maximum_z >= current.minimum_z && current.maximum_z >= candidate->minimum_z;
if (!overlaps_y || !overlaps_z) {
continue;
}
Expand All @@ -125,11 +125,11 @@ std::vector<ContactCandidate> find_cell_contact_candidates(
}
active.push_back(&current);
}
std::ranges::sort(candidates, [&geometry](const ContactCandidate& left,
const ContactCandidate& right) {
return std::tuple{geometry.ids[left.first_slot], geometry.ids[left.second_slot]} <
std::tuple{geometry.ids[right.first_slot], geometry.ids[right.second_slot]};
});
std::ranges::sort(
candidates, [&geometry](const ContactCandidate& left, const ContactCandidate& right) {
return std::tuple{geometry.ids[left.first_slot], geometry.ids[left.second_slot]} <
std::tuple{geometry.ids[right.first_slot], geometry.ids[right.second_slot]};
});
return candidates;
}

Expand Down
11 changes: 5 additions & 6 deletions cpp/core/mechanics_integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ bool finite(const Vec3& value) {
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
}

} // namespace

Vec3 rotate_axis_angle(Vec3 direction, Vec3 rotation, float max_rotation) {
const auto magnitude = norm(rotation);
if (magnitude <= 1.0e-12F || max_rotation == 0.0F) {
Expand All @@ -33,8 +35,6 @@ Vec3 rotate_axis_angle(Vec3 direction, Vec3 rotation, float max_rotation) {
axis * (dot(axis, direction) * (1.0F - cosine)));
}

} // namespace

void validate_mechanics_integration_parameters(const MechanicsIntegrationParameters& parameters) {
if (!std::isfinite(parameters.max_rotation_radians) || parameters.max_rotation_radians < 0.0F) {
throw std::invalid_argument("mechanics rotation limit must be finite and non-negative");
Expand Down Expand Up @@ -74,10 +74,9 @@ void integrate_mechanics_result(WorldState& state, const MechanicsSolveResult& r
const auto applied_length_increment =
cell.fixed ? desired_increment : std::max(0.0F, desired_increment + correction.length);
const auto new_position = cell.fixed ? cell.position : cell.position + correction.translation;
const auto new_direction =
cell.fixed ? cell.direction
: rotate_axis_angle(cell.direction, correction.rotation,
parameters.max_rotation_radians);
const auto new_direction = cell.fixed ? cell.direction
: rotate_axis_angle(cell.direction, correction.rotation,
parameters.max_rotation_radians);
const auto new_length = cell.length + applied_length_increment;
if (!finite(new_position) || !finite(new_direction) || !std::isfinite(new_length)) {
throw std::overflow_error("mechanics integration produced non-finite geometry");
Expand Down
Loading