diff --git a/AGENTS.md b/AGENTS.md index 4627d14b..a314b463 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,11 @@ DEM-Engine is a performance-sensitive CUDA/C++ DEM solver with a Chrono-like pub - Keep Python bindings in sync when changing public C++ APIs that should be exposed to Python. - Preserve backward compatibility unless the requested change explicitly breaks it. +## Asynchronous Output + +- Background writers must own complete snapshots or have explicit synchronization protecting every shared input until use finishes. Include counts, mappings, output settings and mesh caches in the ownership audit, not just device/host arrays. Follow [the asynchronous-output design](docs/developer/async-output.rst). +- Keep completion waits needed before reading or deleting output files; snapshot safety does not imply file completion. + ## Tests And Demos - Prefer modular tests for targeted behavior changes. They should be deterministic, self-checking, and small enough to run as normal build targets. diff --git a/VERSION b/VERSION index 9bbe6516..29cec99e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.14 +3.0.18 diff --git a/cmake/PrepareJitify.cmake b/cmake/PrepareJitify.cmake new file mode 100644 index 00000000..42e087c2 --- /dev/null +++ b/cmake/PrepareJitify.cmake @@ -0,0 +1,30 @@ +# Copy the selected Jitify header without changing the third-party checkout. On MSVC, the optional upstream +# fallback needs the same DbgHelp serialization as DEME's vendored variant. Reject unfamiliar fallback layouts +# rather than silently producing an unprotected Windows build. +function(deme_prepare_jitify input_header output_header msvc_build) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${input_header}") + file(READ "${input_header}" jitify_source) + if(msvc_build AND NOT jitify_source MATCHES "DEME serializes DbgHelp") + set(demangle_signature "inline std::string demangle_native_type(const std::type_info& typeinfo) {") + string(FIND "${jitify_source}" "${demangle_signature}" signature_offset) + if(signature_offset EQUAL -1) + message(FATAL_ERROR "Cannot add MSVC DbgHelp protection to ${input_header}; use DEME's vendored Jitify header.") + endif() + # This signature also occurs in the non-MSVC implementation. Guard the inserted code explicitly so the + # generated header remains usable on other platforms with no locking or extra includes there. + set(demangle_guard "\n#ifdef _MSC_VER + // DEME serializes DbgHelp across all reflected types and worker threads. + static std::mutex dbghelp_mutex; + std::lock_guard lock(dbghelp_mutex); +#endif +") + string(REPLACE "${demangle_signature}" "${demangle_signature}${demangle_guard}" jitify_source "${jitify_source}") + set(jitify_source "#ifdef _MSC_VER\n#include \n#endif\n${jitify_source}") + # COPYONLY keeps the final header's timestamp stable when configure is rerun without content changes. + set(patched_header "${output_header}.msvc-input") + file(WRITE "${patched_header}" "${jitify_source}") + configure_file("${patched_header}" "${output_header}" COPYONLY) + else() + configure_file("${input_header}" "${output_header}" COPYONLY) + endif() +endfunction() diff --git a/docs/README.md b/docs/README.md index 02299c33..a990325b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -84,10 +84,12 @@ cmake --build build-docs --target docs Normal HTML builds use the committed `docs/python/reference.rst` and therefore do not import DEME or require a CUDA GPU. -Regenerate that file after changing pybind11 signatures or docstrings. First -build and stage the Python extension: +Regenerate that file after changing pybind11 signatures or docstrings. From the +repository root, activate the Python environment used to build the extension +and run this complete sequence. Doxygen and Sphinx must also be available: ```bash +# Build and stage the current Python extension. cmake -S . -B build-python \ -DDEME_BUILD_PYTHON=ON \ -DCMAKE_BUILD_TYPE=Release @@ -95,26 +97,45 @@ cmake --build build-python --target _deme --parallel cmake --install build-python \ --component python \ --prefix /tmp/deme-python-reference -``` - -Then run the generator with the same Python interpreter used to build the -extension: -```bash +# Generate the reference using the same Python interpreter. PYTHONPATH=/tmp/deme-python-reference \ - /usr/bin/make -C docs python-reference -``` - -Finally, rebuild the site: + /usr/bin/make -C docs python-reference PYTHON="$(command -v python)" -```bash +# Rebuild the documentation (requires Doxygen and Sphinx). /usr/bin/make -C docs html + +# Review the generated changes. +git diff -- docs/python/reference.rst ``` Do not edit `docs/python/reference.rst` directly. Improve the corresponding docstring in `src/DEM/python/bindings.cpp`, rebuild the extension, and regenerate the page. +## Publish to GitHub Pages + +In **Settings → Secrets and variables → Actions → Variables**, create or update +the repository variable `DOCS_PUBLISH_BRANCH` to the branch that should publish +the documentation. Set it to `develop` to publish from `develop` instead of +`DEME3_docs`. This must be a repository **variable**, not a secret or an +environment variable: the workflow reads `vars.DOCS_PUBLISH_BRANCH`. + +Set **Settings → Pages → Build and deployment → Source** to **GitHub Actions**, +and allow the selected branch in the `github-pages` environment's deployment +branch rules under **Settings → Environments**. + +Commit the documentation sources, including the regenerated Python reference, +and push to the selected branch. The **Build and publish documentation** workflow +builds and publishes the site automatically. Keep generated `docs/_build/` +files out of Git. + +An unset variable or a branch mismatch causes push and manual runs to skip the +build and deployment. Changing the variable alone does not trigger a build; +push a new commit or, when **Run workflow** is available, select the matching +branch and run it manually. Pull requests build for validation without deploying. +See [hosting options](developer/hosting.rst) for the full setup details. + ## Preview on the same machine Run a local HTTP server from the repository root: diff --git a/docs/developer/architecture.rst b/docs/developer/architecture.rst index 16b5f5c0..085aa944 100644 --- a/docs/developer/architecture.rst +++ b/docs/developer/architecture.rst @@ -33,6 +33,9 @@ synchronization. Avoid adding device-wide synchronization to hot paths. When cross-device data is transferred, preserve the lifetime of the source storage until the destination operation has completed. +File-output ownership, current snapshot coverage and remaining host-memory risks +are documented in `Asynchronous output `__. + Adding Python bindings ---------------------- diff --git a/docs/developer/async-output.rst b/docs/developer/async-output.rst new file mode 100644 index 00000000..ad14bf36 --- /dev/null +++ b/docs/developer/async-output.rst @@ -0,0 +1,198 @@ +Asynchronous output: ownership and synchronization +================================================== + +Scope and current status +------------------------ + +This page describes file-output ownership and synchronization requirements. +It separates protections already implemented from the contract intended for all +writers. Asynchronous output does not make the solver API safe for arbitrary +concurrent calls from multiple application threads. + +Current writer ownership +------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 23 42 35 + + * - Writer + - Background inputs + - Protection and remaining limitations + * - ``WriteContactFile`` + - Owned ``ContactInfoContainer``, copied output flags and wildcard names. + - Snapshot completed before return. Subsequent dynamics cannot change the + writer's inputs. Includes the CSV fallback for binary output and the + potential-pair convenience API. + * - ``WriteAnalyticalFile`` + - Owned vector of transformed analytical components; copied domain bounds + and tessellation resolution. + - Snapshots inputs before launching the writer. + * - ``WriteSphereFile`` + - Shared dT host arrays and output metadata, accessed through the solver. + - CSV, binary fallback and VTK still depend on those inputs remaining + unchanged until the writer finishes. + * - ``WriteClumpFile`` + - Shared dT host arrays and output metadata; copied numeric precision. + - CSV and binary fallback still depend on shared inputs remaining unchanged. + * - ``WriteMeshFile`` + - Shared host arrays, mesh caches and output metadata; some options copied. + - VTK, STL and PLY do not yet own complete snapshots. Refreshing a cache + before launch does not isolate it from later mutation. + +See the implementations in `APIPublic.cpp <../../src/DEM/APIPublic.cpp>`__, the +host readers in `dT.cpp <../../src/DEM/dT.cpp>`__, and the snapshot-only contact +formatter in `ContactOutput.hpp <../../src/DEM/utils/ContactOutput.hpp>`__. + +Execution and synchronization +----------------------------- + +The solver has one pending output thread. Each file-output method first calls +``WaitForPendingOutput()``, which joins the previous writer. Output jobs therefore +serialize with each other; they do not each run on an independent concurrent +writer thread. + +The shared-array methods then download the necessary GPU values into the solver's +existing host storage and launch a thread capturing ``this``. Formatting and disk +writing take place on that thread. Capturing ``this`` copies a pointer, not the +arrays. Local vectors or string streams constructed by the writer do not protect +the shared inputs while the writer is constructing them. + +The snapshot methods instead finish reading solver state on the calling thread +and hand owned data to the background thread. Contact output performs filtering, +ID resolution and coordinate transformations while constructing this snapshot; +CSV formatting and disk I/O remain asynchronous. + +``DoDynamics()`` does not join pending output. ``DoDynamicsThenSync()`` synchronizes +the simulation workers, not the file writer. Some mutating operations, including +``Update()`` and the mesh-deformation APIs, explicitly wait for output. Those are +local safeguards, not a general lock around every host access or mutation. + +Data categories and treatment +----------------------------- + +Device arrays +~~~~~~~~~~~~~ + +File writers use CPU-readable data downloaded before launch. Once a transfer has +completed, changing a separate device allocation does not itself change its host +mirror. This is why overlapping GPU dynamics and output can work when host inputs +remain stable. Transfer completion alone does not guarantee that the host mirror +will remain unchanged afterward. + +Shared host mirrors +~~~~~~~~~~~~~~~~~~~ + +The host portions of ``DualArray`` objects belong to the solver. A later download, +host-side setter, resize or storage replacement can change the same memory a +writer is reading. Resizing can also invalidate references or pointers. A host +mirror is not an output-job snapshot merely because it is separate from device +memory. Shared storage requires protection until the last reader finishes. + +Counts, mappings and configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Counts, owner/geometry mappings, family filters, output flags, wildcard names and +coordinate-conversion parameters are part of the output's inputs too. Arrays and +metadata must describe the same frame. Copying a count alone cannot protect an +array that can be overwritten or reallocated; copying an array alone cannot +protect interpretation through mutable mappings or settings. + +Contact output resolves these dependencies before launch and copies its +column configuration. Its background formatter has no solver access. The shared +sphere, clump and mesh writers still consult solver metadata while formatting. + +Mesh caches and setup geometry +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``WriteMeshFile()`` synchronizes deformed mesh caches before starting its writer, +but the writer still reads solver-owned meshes and owner transforms. Treat vertex +and connectivity vectors, patch data and any output attributes as dependencies. +Setup-time data may be shared only if its lifetime and immutability are guaranteed +for the entire write. Do not infer that guarantee merely because a vector usually +stays unchanged during dynamics. Direct mutation of exposed setup/cache objects +also needs to be considered when reviewing a writer. + +Job-owned snapshots +~~~~~~~~~~~~~~~~~~~ + +A complete snapshot owns the values needed to interpret and serialize a frame. +It must not retain pointers, references or views into mutable solver storage. +Contact output holds an independently populated container through a const shared +pointer; analytical output moves its component vector into the writer. These +jobs keep their storage alive until formatting and writing finish. + +Contact snapshot construction +----------------------------- + +``WriteContactFile()`` calls ``generateContactInfo()`` before launching the +writer and captures the resulting owned container, output flags and wildcard +names. Both normal CSV and the binary-to-CSV fallback use the same snapshot-only +formatter. Invalid contact types are rejected during snapshot construction. + +Remaining risks and caller guidance +----------------------------------- + +Sphere, clump and mesh output have not yet been converted to complete snapshots. +Their shared inputs can cause inconsistent frames, invalid indexing or invalid +memory access if changed concurrently. This is an ownership risk identified from +the implementation, not a claim that every ordinary dynamics call reproduces a +failure in each writer. Stable owner counts and GPU-only updates can leave their +host inputs undisturbed, and some APIs already wait before mutation. + +Until those writers are isolated, callers can explicitly finish output before +resuming dynamics or performing operations that could change its shared inputs: + +.. code-block:: cpp + + solver.WriteMeshFile("frame.vtk"); // Same precaution for sphere/clump output. + solver.WaitForPendingOutput(); + solver.DoDynamicsThenSync(duration); + +Contact and analytical output do not need that wait solely to protect their +snapshots from subsequent dynamics. All asynchronous outputs still need a +completion wait before the application reads, checks, deletes or otherwise +consumes the destination file. An owned snapshot guarantees input stability; +it does not make the file complete when the API returns. + +Required contract and follow-up work +------------------------------------ + +Every background writer must either own its complete inputs or have explicit +synchronization protecting all shared inputs for their entire use. The preferred +follow-up is to give sphere, clump and mesh output complete owned snapshots, +including any metadata needed for serialization. These writers currently retain +the shared-storage behavior described above. + +Build snapshots while the relevant solver state is stable, then pass only those +snapshots to standalone formatting code. Preserve file layouts, filtering, +coordinate conventions and format fallbacks. Snapshot creation adds foreground +CPU work, but should leave formatting and disk writing asynchronous and avoid +unnecessary device-wide synchronization in dynamics. Assess memory use for large +outputs, including snapshot and formatting buffers. + +Ownership does not solve every output concern. File-open/write failures and +exceptions on the background thread need a separate error-propagation policy; +the snapshot mechanism does not provide one. Completion waits, solver lifetime and the +supported application-thread calling model also remain part of the contract. + +Regression coverage +------------------- + +`DEMTest_ContactOutputSnapshot.cpp <../../src/demo/ModularTests/DEMTest_ContactOutputSnapshot.cpp>`__ +uses a POSIX FIFO to block the writer opening its destination until after dynamics +changes the contact list. It compares the delayed output byte-for-byte with a +reference from the requested frame. Coverage includes contact-list growth, CSV, +binary fallback with potential pairs, force filtering to zero rows and contact +wildcards. The FIFO test skips on Windows; the production snapshot mechanism is +platform-independent. + +The test deliberately has no completion wait between queuing the delayed contact +output and advancing dynamics. Its other waits are required to finish the +reference file before reading it and to join the delayed writer after draining +the FIFO. Removing those waits would not be a valid test of snapshot safety. + +Future writer conversions should receive equivalent delayed-output tests covering +their mutable arrays, metadata and caches. Do not use sleeps as the only mechanism +for provoking a race, and do not treat contact-test success as coverage of the +remaining shared-array writers. diff --git a/docs/index.rst b/docs/index.rst index 69f1b787..0aa11b62 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,6 +33,7 @@ kernels. :caption: Development developer/architecture + developer/async-output developer/type-codes developer/documentation developer/packaging diff --git a/docs/python/reference.rst b/docs/python/reference.rst index 087dddf6..ff33bc31 100644 --- a/docs/python/reference.rst +++ b/docs/python/reference.rst @@ -81,6 +81,10 @@ DEMSolver object alive while using any solver-owned material, template, tracker, or inspector handle. + Fixed-size owner CUDA exchange methods validate ranges, capacities, and pointer metadata by default. Pass + ``validate=False`` only when those preconditions are guaranteed and avoiding validation overhead matters. Required + device routing and documented transformations still apply. + .. py:method:: DEMSolver.__init__ .. code-block:: text @@ -209,6 +213,22 @@ DEMSolver Add an extra acceleration to a owner for the next time step. +.. py:method:: DEMSolver.AddOwnerNextStepAccFromDevice + + .. code-block:: text + + AddOwnerNextStepAccFromDevice(self: deme._deme.DEMSolver, owner_id: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously queue global-frame linear accelerations for consecutive owners from CUDA memory. + + ``source`` is an integer address containing ``count`` CUDA ``float3`` values. Remote input is copied from + ``source_device`` to the dynamic-worker device before unpacking. Values replace any previously queued next-step + linear-acceleration contribution. Contact acceleration is accumulated on top during the next force/integration step, + gravity is applied separately, and the queued contribution is consumed after one step. Pass ``validate=False`` only + when the owner range and pointer metadata are known to be valid. A zero count is a no-op; otherwise the source buffer + may be reused when this call returns. + + .. py:method:: DEMSolver.AddOwnerNextStepAngAcc .. code-block:: text @@ -218,6 +238,22 @@ DEMSolver Add an extra angular acceleration to a owner for the next time step. +.. py:method:: DEMSolver.AddOwnerNextStepAngAccFromDevice + + .. code-block:: text + + AddOwnerNextStepAngAccFromDevice(self: deme._deme.DEMSolver, owner_id: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously queue local-frame angular accelerations for consecutive owners from CUDA memory. + + ``source`` is an integer address containing ``count`` CUDA ``float3`` values. Remote input is copied from + ``source_device`` to the dynamic-worker device before unpacking. Values use each owner's local principal-axis frame + and replace any previously queued next-step angular-acceleration contribution. Contact angular acceleration is + accumulated on top during the next force/integration step, and the queued contribution is consumed after one step. Pass + ``validate=False`` only when the owner range and pointer metadata are known to be valid. A zero count is a no-op; + otherwise the source buffer may be reused when this call returns. + + .. py:method:: DEMSolver.AddShellMesh .. code-block:: text @@ -384,7 +420,7 @@ DEMSolver DisableAdaptiveBinSize(self: deme._deme.DEMSolver) -> None - Disable the use of adaptive bin size (always use initial size) + Disable adaptive bin size. After initialization, call UpdateSimParams() for this change to take effect in the workers. .. py:method:: DEMSolver.DisableAdaptiveUpdateFreq @@ -393,7 +429,7 @@ DEMSolver DisableAdaptiveUpdateFreq(self: deme._deme.DEMSolver) -> None - Disable the use of adaptive max update step count (always use initial update frequency) + Disable adaptive contact-update frequency. After initialization, call UpdateSimParams() for this change to take effect in the workers. .. py:method:: DEMSolver.DisableAllMeshWearModels @@ -843,6 +879,15 @@ DEMSolver Get the acceleration of n consecutive owners. +.. py:method:: DEMSolver.GetOwnerAccToDevice + + .. code-block:: text + + GetOwnerAccToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive global contact accelerations as CUDA float3 elements. + + .. py:method:: DEMSolver.GetOwnerAngAcc .. code-block:: text @@ -852,6 +897,24 @@ DEMSolver Get the angular acceleration of n consecutive owners. +.. py:method:: DEMSolver.GetOwnerAngAccGlobalToDevice + + .. code-block:: text + + GetOwnerAngAccGlobalToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive global contact angular accelerations as CUDA float3 elements. + + +.. py:method:: DEMSolver.GetOwnerAngAccLocalToDevice + + .. code-block:: text + + GetOwnerAngAccLocalToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive local principal-axis-frame contact angular accelerations as CUDA float3 elements. + + .. py:method:: DEMSolver.GetOwnerAngVel .. code-block:: text @@ -861,6 +924,24 @@ DEMSolver Get angular velocity of n consecutive owners. +.. py:method:: DEMSolver.GetOwnerAngVelGlobalToDevice + + .. code-block:: text + + GetOwnerAngVelGlobalToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive global owner angular velocities as CUDA float3 elements. + + +.. py:method:: DEMSolver.GetOwnerAngVelLocalToDevice + + .. code-block:: text + + GetOwnerAngVelLocalToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive local principal-axis-frame owner angular velocities as CUDA float3 elements. + + .. py:method:: DEMSolver.GetOwnerContactClumps .. code-block:: text @@ -886,6 +967,40 @@ DEMSolver Get all contact forces and torque that concern a list of owners. +.. py:method:: DEMSolver.GetOwnerContactWrench + + .. code-block:: text + + GetOwnerContactWrench(self: deme._deme.DEMSolver, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1) -> tuple[list[float3], list[float3]] + + Return one reduced contact wrench per consecutive owner. + + The returned tuple is ``(forces, torques)``; each vector is resized to ``count`` and follows consecutive owner order. + Force and torque are global-frame resultants, and torque is measured about DEME's current owner position. Torque + includes both force-generated moments and force-model-only torque such as rolling resistance. Owners without recorded + contact receive zero force and torque. This reads current dT force records and does not trigger contact detection or + force evaluation. ``first_owner_id`` selects the first owner in the range. Contact recording must remain enabled. A + zero count returns two empty vectors. + + +.. py:method:: DEMSolver.GetOwnerContactWrenchToDevice + + .. code-block:: text + + GetOwnerContactWrenchToDevice(self: deme._deme.DEMSolver, force_destination: typing.SupportsInt | typing.SupportsIndex, torque_destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1) -> None + + Synchronously write one reduced contact wrench per consecutive owner to CUDA ``float3`` buffers. + + Force and torque are global-frame resultants, and torque is measured about DEME's current owner position. Torque + includes both force-generated moments and force-model-only torque such as rolling resistance. ``force_destination`` + and ``torque_destination`` are integer addresses of writable CUDA ``float3`` storage. ``capacity`` and ``count`` are + measured in owners, and ``first_owner_id`` selects the start of the range; owners without contact receive zero force + and torque. This reads current dT force records and does not trigger contact detection or force evaluation. Both + buffers belong to logical CUDA ``destination_device``; CUDA selects the available inter-device transfer route for + remote output. The call is synchronous, so both buffers may be consumed when it returns. Contact recording must remain + enabled. A zero count is a no-op. + + .. py:method:: DEMSolver.GetOwnerFamily .. code-block:: text @@ -895,6 +1010,15 @@ DEMSolver Get the family number of n consecutive owners. +.. py:method:: DEMSolver.GetOwnerFamilyToDevice + + .. code-block:: text + + GetOwnerFamilyToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive owner family numbers as CUDA uint32 elements. + + .. py:method:: DEMSolver.GetOwnerMass .. code-block:: text @@ -904,6 +1028,15 @@ DEMSolver Get the mass of n consecutive owners. +.. py:method:: DEMSolver.GetOwnerMassToDevice + + .. code-block:: text + + GetOwnerMassToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive owner masses as CUDA float elements. + + .. py:method:: DEMSolver.GetOwnerMOI .. code-block:: text @@ -913,6 +1046,15 @@ DEMSolver Get the moment of inertia (in principal axis frame) of n consecutive owners. +.. py:method:: DEMSolver.GetOwnerMOIToDevice + + .. code-block:: text + + GetOwnerMOIToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive owner principal moments of inertia as CUDA float3 elements. + + .. py:method:: DEMSolver.GetOwnerOriQ .. code-block:: text @@ -922,6 +1064,15 @@ DEMSolver Get quaternion of n consecutive owners. +.. py:method:: DEMSolver.GetOwnerOriQToDevice + + .. code-block:: text + + GetOwnerOriQToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive owner quaternions as CUDA float4 elements in (x, y, z, w) order. + + .. py:method:: DEMSolver.GetOwnerPosition .. code-block:: text @@ -931,6 +1082,15 @@ DEMSolver Get position of n consecutive owners. +.. py:method:: DEMSolver.GetOwnerPositionToDevice + + .. code-block:: text + + GetOwnerPositionToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive global owner positions as CUDA float3 elements. + + .. py:method:: DEMSolver.GetOwnerVelocity .. code-block:: text @@ -940,6 +1100,15 @@ DEMSolver Get velocity of n consecutive owners. +.. py:method:: DEMSolver.GetOwnerVelocityToDevice + + .. code-block:: text + + GetOwnerVelocityToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write consecutive global owner velocities as CUDA float3 elements. + + .. py:method:: DEMSolver.GetOwnerWildcardValue .. code-block:: text @@ -949,6 +1118,15 @@ DEMSolver Get the owner wildcard's values of some owners. +.. py:method:: DEMSolver.GetOwnerWildcardValueToDevice + + .. code-block:: text + + GetOwnerWildcardValueToDevice(self: deme._deme.DEMSolver, destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, destination_device: typing.SupportsInt | typing.SupportsIndex, first_owner_id: typing.SupportsInt | typing.SupportsIndex, name: str, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously write one named owner wildcard for consecutive owners as CUDA float elements. + + .. py:method:: DEMSolver.GetSimTime .. code-block:: text @@ -985,6 +1163,20 @@ DEMSolver Get the current update frequency used by the solver. +.. py:method:: DEMSolver.GetVisualizationFrame + + .. code-block:: text + + GetVisualizationFrame(self: deme._deme.DEMSolver, frame: deme._deme.VisualizationFrame, include_velocities: bool = False) -> None + + +.. py:method:: DEMSolver.GetVisualizationScene + + .. code-block:: text + + GetVisualizationScene(self: deme._deme.DEMSolver) -> deme._deme.VisualizationScene + + .. py:method:: DEMSolver.GetWhetherForceCollectInKernel .. code-block:: text @@ -1267,7 +1459,7 @@ DEMSolver SetAdaptiveBinSizeAcc(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None - Set how fast kT changes the direction of bin size adjustmemt when there's a more beneficial direction + Set the bin-size adjustment acceleration. After initialization, call UpdateSimParams(). .. py:method:: DEMSolver.SetAdaptiveBinSizeDelaySteps @@ -1276,7 +1468,7 @@ DEMSolver SetAdaptiveBinSizeDelaySteps(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None - Adjust how frequent kT updates the bin size + Adjust how frequently kT updates the bin size. After initialization, call UpdateSimParams(). .. py:method:: DEMSolver.SetAdaptiveBinSizeLowerProactivity @@ -1285,7 +1477,7 @@ DEMSolver SetAdaptiveBinSizeLowerProactivity(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None - Set how proactive the solver is in avoiding the bin being too small (leading to too many bins in domain). + Set how proactive the solver is in avoiding undersized bins. After initialization, call UpdateSimParams(). .. py:method:: DEMSolver.SetAdaptiveBinSizeMaxRate @@ -1294,7 +1486,7 @@ DEMSolver SetAdaptiveBinSizeMaxRate(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None - Set the max rate that the bin size can change in one adjustment + Set the maximum bin-size change rate. After initialization, call UpdateSimParams(). .. py:method:: DEMSolver.SetAdaptiveBinSizeUpperProactivity @@ -1303,7 +1495,7 @@ DEMSolver SetAdaptiveBinSizeUpperProactivity(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None - Set how proactive the solver is in avoiding the bin being too big (leading to too many geometries in a bin) + Set how proactive the solver is in avoiding the bin being too big (leading to too many geometries in a bin). After initialization, call UpdateSimParams(). .. py:method:: DEMSolver.SetAdaptiveTimeStepType @@ -1330,7 +1522,7 @@ DEMSolver SetCDMaxUpdateFreq(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None - Set the upper bound of kT update frequency (when it is adjusted automatically). + Set the upper bound of kT update frequency. After initialization, call UpdateSimParams(). .. py:method:: DEMSolver.SetCDNumStepsMaxDriftAheadOfAvg @@ -1339,7 +1531,7 @@ DEMSolver SetCDNumStepsMaxDriftAheadOfAvg(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None - Set the number of steps dT configures its max drift more than average drift steps. + Set dT's drift allowance above average. After initialization, call UpdateSimParams(). .. py:method:: DEMSolver.SetCDNumStepsMaxDriftHistorySize @@ -1348,7 +1540,7 @@ DEMSolver SetCDNumStepsMaxDriftHistorySize(self: deme._deme.DEMSolver, n: typing.SupportsInt | typing.SupportsIndex) -> None - Set how many past kinematic-worker updates calibrate the maximum future drift limit. The default is recommended for normal use. + Set how many past kinematic-worker updates calibrate the maximum future drift limit. The default is recommended for normal use. After initialization, call UpdateSimParams(). .. py:method:: DEMSolver.SetCDNumStepsMaxDriftMultipleOfAvg @@ -1357,7 +1549,7 @@ DEMSolver SetCDNumStepsMaxDriftMultipleOfAvg(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None - Set the multiplier which dT configures its max drift to be w.r.t. the average drift steps. + Set dT's maximum-drift multiplier. After initialization, call UpdateSimParams(). .. py:method:: DEMSolver.SetCDUpdateFreq @@ -1366,7 +1558,7 @@ DEMSolver SetCDUpdateFreq(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None - Set the number of dT steps before it waits for a contact-pair info update from kT. + Set the number of dT steps before it waits for a contact-pair update. After initialization, call UpdateSimParams() for this change to take effect in the workers. .. py:method:: DEMSolver.SetCollectAccRightAfterForceCalc @@ -1375,7 +1567,7 @@ DEMSolver SetCollectAccRightAfterForceCalc(self: deme._deme.DEMSolver, flag: bool = True) -> None - Reduce contact forces to accelerations right after calculating them, in the same kernel. This may give some performance boost if you have only polydisperse spheres, no clumps. + Reduce contact forces to accelerations right after calculating them, in the same kernel. This may give some performance boost if you have only polydisperse spheres, no clumps. After initialization, call UpdateSimParams() for this change to take effect in dT. .. py:method:: DEMSolver.SetContactOutputContent @@ -1423,6 +1615,15 @@ DEMSolver Set the process-wide switch that synchronizes after each CUDA operation that enqueues stream work. Disable for normal asynchronous performance. +.. py:method:: DEMSolver.SetDEME2MeshBehavior + + .. code-block:: text + + SetDEME2MeshBehavior(self: deme._deme.DEMSolver, use: bool = True) -> None + + When enabled, assign every triangle of subsequently loaded meshes to its own patch. Disabling restores the normal behavior of leaving new meshes' patch assignments unchanged. + + .. py:method:: DEMSolver.SetErrorOutAngularVelocity .. code-block:: text @@ -1463,7 +1664,7 @@ DEMSolver SetExpandFactor(self: deme._deme.DEMSolver, beta: typing.SupportsFloat | typing.SupportsIndex, fix: bool = True) -> None - (Explicitly) set the amount by which the radii of the spheres (and the thickness of the boundaries) are expanded for the purpose of contact detection (safe, and creates false positives). If fix is set to true, then this expand factor does not change even if the user uses variable time step size. + (Explicitly) set the amount by which the radii of the spheres (and the thickness of the boundaries) are expanded for the purpose of contact detection (safe, and creates false positives). If fix is set to true, then this expand factor does not change even if the user uses variable time step size. After initialization, call UpdateSimParams() for this change to take effect in the workers. .. py:method:: DEMSolver.SetExpandSafetyAdder @@ -1472,7 +1673,7 @@ DEMSolver SetExpandSafetyAdder(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None - Set a `base' velocity, which we will always add to our estimated maximum system velocity, when deriving the thickness of the contact `safety' margin + Set a `base' velocity, which we will always add to our estimated maximum system velocity, when deriving the thickness of the contact `safety' margin. After initialization, call UpdateSimParams() for this change to take effect in the workers. .. py:method:: DEMSolver.SetExpandSafetyMultiplier @@ -1481,7 +1682,7 @@ DEMSolver SetExpandSafetyMultiplier(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None - Assign a multiplier to our estimated maximum system velocity, when deriving the thinckness of the contact `safety' margin. + Assign a multiplier to our estimated maximum system velocity, when deriving the thinckness of the contact `safety' margin. After initialization, call UpdateSimParams() for this change to take effect in the workers. .. py:method:: DEMSolver.SetExpandSafetyType @@ -1490,7 +1691,7 @@ DEMSolver SetExpandSafetyType(self: deme._deme.DEMSolver, arg0: str) -> None - A string. If 'auto': the solver automatically derives. + Select the contact-margin velocity strategy. After initialization, call UpdateSimParams() for this change to take effect in the workers. .. py:method:: DEMSolver.SetFamilyClumpMaterial @@ -1741,7 +1942,7 @@ DEMSolver SetGravitationalAcceleration(self: deme._deme.DEMSolver, acc: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - Set the global gravitational acceleration as ``[x, y, z]`` in length/time^2 units. + Set the global gravitational acceleration as ``[x, y, z]`` in length/time^2 units. This takes effect immediately after initialization when called while the solver is synchronized. .. py:method:: DEMSolver.SetInitBinNumTarget @@ -1777,7 +1978,7 @@ DEMSolver SetInitTimeStep(self: deme._deme.DEMSolver, ts: typing.SupportsFloat | typing.SupportsIndex) -> None - Set the initial timestep in seconds before ``Initialize``. A constant-step simulation uses it throughout; an adaptive strategy may update it later. + Legacy name for SetTimeStepSize, kept for backward compatibility. .. py:method:: DEMSolver.SetIntegrator @@ -1789,11 +1990,11 @@ DEMSolver 1. SetIntegrator(self: deme._deme.DEMSolver, arg0: str) -> None - Set the time integrator for this simulator. + Set the time integrator. After initialization, call UpdateSimParams() for this change to take effect. 2. SetIntegrator(self: deme._deme.DEMSolver, arg0: deme::TIME_INTEGRATOR) -> None - Set the time integrator for this simulator. + Set the time integrator. After initialization, call UpdateSimParams() for this change to take effect. .. py:method:: DEMSolver.SetJitifyClumpTemplates @@ -1874,7 +2075,23 @@ DEMSolver SetMaxVelocity(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None - Set the maximum expected particle velocity. The solver will not use a velocity larger than this for determining the margin thickness, and velocity larger than this will be considered a system anomaly. + Set the maximum expected particle velocity. The solver will not use a velocity larger than this for determining the margin thickness, and velocity larger than this will be considered a system anomaly. After initialization, call UpdateSimParams() for this change to take effect in the workers. + + +.. py:method:: DEMSolver.SetMeshOutputContent + + .. code-block:: text + + SetMeshOutputContent(*args, **kwargs) + Overloaded function. + + 1. SetMeshOutputContent(self: deme._deme.DEMSolver, content: collections.abc.Sequence[str]) -> None + + Specify per-triangle fields to include in mesh VTK output. + + 2. SetMeshOutputContent(self: deme._deme.DEMSolver, content: deme::MESH_OUTPUT_CONTENT) -> None + + Specify one per-triangle field to include in mesh VTK output. .. py:method:: DEMSolver.SetMeshOutputFormat @@ -1895,6 +2112,24 @@ DEMSolver Declare that all meshed particles have a low polygon count (e.g. box, tetrahedron). When enabled, the per-triangle maxTriTriPenetration array is neither computed, transferred to kT, nor used to inflate contact-detection margins, saving compute time. Toggle this on only when you are confident that no triangle from one mesh will ever be completely submerged inside another mesh. +.. py:method:: DEMSolver.SetMeshPatchLocations + + .. code-block:: text + + SetMeshPatchLocations(self: deme._deme.DEMSolver, owner: typing.SupportsInt | typing.SupportsIndex, centers: collections.abc.Sequence[float3]) -> None + + Set one local center per patch at runtime and select user-managed centers. + + +.. py:method:: DEMSolver.SetMeshPatchLocationsFromDevice + + .. code-block:: text + + SetMeshPatchLocationsFromDevice(self: deme._deme.DEMSolver, owner: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously set user-managed local patch centers from one CUDA float3 per patch. + + .. py:method:: DEMSolver.SetMeshUniversalContact .. code-block:: text @@ -1910,7 +2145,7 @@ DEMSolver SetNoForceRecord(self: deme._deme.DEMSolver, flag: bool = True) -> None - Instruct the solver that there is no need to record the contact force (and contact point location etc.) in an array. + Instruct the solver that there is no need to record the contact force (and contact point location etc.) in an array. After initialization, call UpdateSimParams() for this change to take effect in dT. .. py:method:: DEMSolver.SetOutputContent @@ -1926,9 +2161,16 @@ DEMSolver .. code-block:: text - SetOutputFormat(self: deme._deme.DEMSolver, arg0: str) -> None + SetOutputFormat(*args, **kwargs) + Overloaded function. + + 1. SetOutputFormat(self: deme._deme.DEMSolver, format: str) -> None + + Choose sphere and clump output file format by name. - Choose sphere and clump output file format. + 2. SetOutputFormat(self: deme._deme.DEMSolver, format: deme::OUTPUT_FORMAT) -> None + + Choose sphere and clump output file format. VTK is supported by WriteSphereFile. .. py:method:: DEMSolver.SetOwnerAngVel @@ -1940,6 +2182,32 @@ DEMSolver Set angular velocity of consecutive owners starting from ownerID, based on input angular velocity vector. N (the size of the input vector) elements will be modified. +.. py:method:: DEMSolver.SetOwnerAngVelFromDevice + + .. code-block:: text + + SetOwnerAngVelFromDevice(self: deme._deme.DEMSolver, owner_id: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously set consecutive local-frame angular velocities from CUDA ``float3`` elements. + + This has the same frame semantics as ``SetOwnerAngVel``. ``source_device`` identifies the CUDA-accessible source + allocation; remote input is copied to the dynamic-worker device before unpacking. When this call returns the source + buffer may be reused. + + +.. py:method:: DEMSolver.SetOwnerAngVelGlobalFromDevice + + .. code-block:: text + + SetOwnerAngVelGlobalFromDevice(self: deme._deme.DEMSolver, owner_id: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously set consecutive global angular velocities from ``count`` CUDA ``float3`` elements. + + Each value is converted to the owner's local principal-axis frame using that owner's orientation at call time. + ``source_device`` identifies the CUDA-accessible source allocation; remote input is copied to the dynamic-worker + device before unpacking. When this call returns the source buffer may be reused. + + .. py:method:: DEMSolver.SetOwnerFamily .. code-block:: text @@ -1958,6 +2226,20 @@ DEMSolver Set quaternion of consecutive owners starting from ownerID, based on input quaternion vector. N (the size of the input vector) elements will be modified. +.. py:method:: DEMSolver.SetOwnerOriQFromDevice + + .. code-block:: text + + SetOwnerOriQFromDevice(self: deme._deme.DEMSolver, owner_id: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously set consecutive owner orientations from ``count`` CUDA ``float4`` elements. + + Quaternion elements use public DEME ordering ``(x, y, z, w)``. ``source_device`` identifies the CUDA-accessible source + allocation; remote input is copied to the dynamic-worker device before validation and unpacking. With + ``validate=True``, non-finite and zero-length quaternions are rejected. Every finite, nonzero quaternion is normalized + before storage even when validation is disabled. When this call returns the source buffer may be reused. + + .. py:method:: DEMSolver.SetOwnerPosition .. code-block:: text @@ -1967,6 +2249,18 @@ DEMSolver Set position of consecutive owners starting from ownerID, based on input position vector. N (the size of the input vector) elements will be modified. +.. py:method:: DEMSolver.SetOwnerPositionFromDevice + + .. code-block:: text + + SetOwnerPositionFromDevice(self: deme._deme.DEMSolver, owner_id: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously set consecutive owner positions from ``count`` CUDA ``float3`` elements. + + Positions are global-frame values. ``source_device`` identifies the CUDA-accessible source allocation; remote input is + copied to the dynamic-worker device before unpacking. When this call returns the source buffer may be reused. + + .. py:method:: DEMSolver.SetOwnerVelocity .. code-block:: text @@ -1976,6 +2270,18 @@ DEMSolver Set velocity of consecutive owners starting from ownerID, based on input velocity vector. N (the size of the input vector) elements will be modified. +.. py:method:: DEMSolver.SetOwnerVelocityFromDevice + + .. code-block:: text + + SetOwnerVelocityFromDevice(self: deme._deme.DEMSolver, owner_id: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, count: typing.SupportsInt | typing.SupportsIndex = 1, validate: bool = True) -> None + + Synchronously set consecutive global linear velocities from ``count`` CUDA ``float3`` elements. + + ``source_device`` identifies the CUDA-accessible source allocation; remote input is copied to the dynamic-worker + device before unpacking. When this call returns the source buffer may be reused. + + .. py:method:: DEMSolver.SetOwnerWildcards .. code-block:: text @@ -2046,6 +2352,15 @@ DEMSolver Stabilize flooded patch-island IDs across contact-detection steps. +.. py:method:: DEMSolver.SetTimeStepSize + + .. code-block:: text + + SetTimeStepSize(self: deme._deme.DEMSolver, ts: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the timestep in seconds before or after initialization. A post-initialization call takes effect on the next step and must be made while the solver is synchronized. + + .. py:method:: DEMSolver.SetTrianglePVTrackingOwners .. code-block:: text @@ -2059,9 +2374,18 @@ DEMSolver .. code-block:: text - SetTriNodeRelPos(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: collections.abc.Sequence[float3]) -> None + SetTriNodeRelPos(self: deme._deme.DEMSolver, owner: typing.SupportsInt | typing.SupportsIndex, triID: typing.SupportsInt | typing.SupportsIndex, new_nodes: collections.abc.Sequence[float3], update_patch_centers: bool = True) -> None - Rewrite the relative positions of the flattened triangle soup. + Rewrite local mesh node coordinates. update_patch_centers refreshes automatic centers from all accumulated deformation; false preserves current centers. It has no effect for user-supplied centers. Automatic single-patch centers remain at the local origin. + + +.. py:method:: DEMSolver.SetTriNodeRelPosFromDevice + + .. code-block:: text + + SetTriNodeRelPosFromDevice(self: deme._deme.DEMSolver, owner: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True, update_patch_centers: bool = True) -> None + + Synchronous local-node device update: one float3 per node, fixed topology. Finish producing source before calling. validate checks pointer and finite values. update_patch_centers refreshes automatic centers only and has no effect for user-supplied centers. Single-patch automatic centers stay at the local origin. Tracker GetMesh/GetMeshNodesGlobal synchronize the lazy CPU geometry cache. .. py:method:: DEMSolver.SetTriTriContactRejectionRatio @@ -2185,16 +2509,25 @@ DEMSolver UpdateStepSize(self: deme._deme.DEMSolver, ts: typing.SupportsFloat | typing.SupportsIndex = -1.0) -> None - Update the time step size. Used after system initialization. + Legacy name for SetTimeStepSize, kept for backward compatibility. .. py:method:: DEMSolver.UpdateTriNodeRelPos .. code-block:: text - UpdateTriNodeRelPos(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: collections.abc.Sequence[float3]) -> None + UpdateTriNodeRelPos(self: deme._deme.DEMSolver, owner: typing.SupportsInt | typing.SupportsIndex, triID: typing.SupportsInt | typing.SupportsIndex, updates: collections.abc.Sequence[float3], update_patch_centers: bool = True) -> None + + Update local mesh node coordinates. update_patch_centers refreshes automatic centers from all accumulated deformation; false preserves current centers. It has no effect for user-supplied centers. Automatic single-patch centers remain at the local origin. + + +.. py:method:: DEMSolver.UpdateTriNodeRelPosFromDevice + + .. code-block:: text + + UpdateTriNodeRelPosFromDevice(self: deme._deme.DEMSolver, owner: typing.SupportsInt | typing.SupportsIndex, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True, update_patch_centers: bool = True) -> None - Update the relative positions of the flattened triangle soup. + Synchronous local-node displacement increment: one float3 per node, fixed topology. Finish producing source before calling. validate checks pointer and finite values. update_patch_centers refreshes automatic centers only and has no effect for user-supplied centers. Single-patch automatic centers stay at the local origin. A later enabled refresh includes skipped deformation. Tracker GetMesh/GetMeshNodesGlobal synchronize the lazy CPU geometry cache. .. py:method:: DEMSolver.UseAdaptiveBinSize @@ -2203,7 +2536,7 @@ DEMSolver UseAdaptiveBinSize(self: deme._deme.DEMSolver, use: bool = True) -> None - Enable or disable the use of adaptive bin size (by default it is on) + Enable or disable adaptive bin size. After initialization, call UpdateSimParams() for this change to take effect in the workers. .. py:method:: DEMSolver.UseAdaptiveUpdateFreq @@ -2212,7 +2545,16 @@ DEMSolver UseAdaptiveUpdateFreq(self: deme._deme.DEMSolver, use: bool = True) -> None - Enable or disable the use of adaptive max update step count (by default it is on) + Enable or disable adaptive contact-update frequency. After initialization, call UpdateSimParams() for this change to take effect in the workers. + + +.. py:method:: DEMSolver.UseAutomaticMeshPatchLocations + + .. code-block:: text + + UseAutomaticMeshPatchLocations(self: deme._deme.DEMSolver, owner: typing.SupportsInt | typing.SupportsIndex) -> None + + Resume automatic centers and immediately recompute from current geometry. .. py:method:: DEMSolver.UseFrictionalHertzianModel @@ -2251,6 +2593,15 @@ DEMSolver Wait for any in-flight async output to finish. +.. py:method:: DEMSolver.WriteAnalyticalFile + + .. code-block:: text + + WriteAnalyticalFile(self: deme._deme.DEMSolver, outfilename: str, circumferential_resolution: typing.SupportsInt | typing.SupportsIndex = 32) -> None + + Write analytical boundary surfaces as VTK clipped to the user-specified domain. + + .. py:method:: DEMSolver.WriteClumpFile .. code-block:: text @@ -2291,2454 +2642,2562 @@ DEMSolver .. code-block:: text - WriteSphereFile(self: deme._deme.DEMSolver, arg0: str) -> None + WriteSphereFile(self: deme._deme.DEMSolver, outfilename: str) -> None - Writes the current status of clumps (but decomposed as spheres) file. + Write clumps as component spheres using the selected CSV or VTK output format. -Tracker -------- +DEMVisualizer +------------- -.. py:class:: Tracker +.. py:class:: DEMVisualizer .. code-block:: text - Access and modify owners associated with a tracked batch or object. - - Create trackers with ``DEMSolver.Track`` during setup. Most state queries and - updates require the solver to be initialized and synchronized. ``offset`` - selects an owner within the tracked collection, starting at zero. Keep the - parent solver alive while using a tracker. + Step-wise interactive viewer. Render() draws the current solver state without advancing the simulation. -.. py:method:: Tracker.__init__ +.. py:method:: DEMVisualizer.__init__ .. code-block:: text - __init__(self: deme._deme.Tracker, solver: deme::DEMSolver) -> None + __init__(self: deme._deme.DEMVisualizer, solver: deme._deme.DEMSolver) -> None - Create a tracker associated with ``solver``. Prefer ``DEMSolver.Track`` so the tracked owner range is configured correctly. + Create a visualizer and keep its solver alive for the lifetime of the viewer. -.. py:method:: Tracker.AddAcc +.. py:method:: DEMVisualizer.Close .. code-block:: text - AddAcc(*args, **kwargs) - Overloaded function. + Close(self: deme._deme.DEMVisualizer) -> None - 1. AddAcc(self: deme._deme.Tracker, acc: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + Close the visualization window. - Add an extra acc to the tracked body, for the next time step. Note if the user intends to add a persistent external force, then using family prescription is the better method. - 2. AddAcc(self: deme._deme.Tracker, acc: collections.abc.Sequence[float3]) -> None +.. py:method:: DEMVisualizer.FrameAll - Add an extra acc to consecutive tracked objects, (only) for the next time step. Note if the user intends to add a persistent external force, then using family prescription is the better method. + .. code-block:: text + FrameAll(self: deme._deme.DEMVisualizer) -> None -.. py:method:: Tracker.AddAngAcc + +.. py:method:: DEMVisualizer.FrameSelected .. code-block:: text - AddAngAcc(*args, **kwargs) - Overloaded function. + FrameSelected(self: deme._deme.DEMVisualizer) -> None - 1. AddAngAcc(self: deme._deme.Tracker, angAcc: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None - Add an extra angular acceleration to the tracked body, for the next time step. Note if the user intends to add a persistent external torque, then using family prescription is the better method. +.. py:method:: DEMVisualizer.GetSelectedGeometryID - 2. AddAngAcc(self: deme._deme.Tracker, angAcc: collections.abc.Sequence[float3]) -> None + .. code-block:: text - Add an extra angular acceleration to consecutive tracked objects, (only) for the next time step. Note if the user intends to add a persistent external torque, then using family prescription is the better method. + GetSelectedGeometryID(self: deme._deme.DEMVisualizer) -> int -.. py:method:: Tracker.AngularVelocitiesGlobal +.. py:method:: DEMVisualizer.GetSelectedOwner .. code-block:: text - AngularVelocitiesGlobal(self: deme._deme.Tracker) -> list[list[float]] - - Return global-frame angular velocities for all tracked owners. + GetSelectedOwner(self: deme._deme.DEMVisualizer) -> int -.. py:method:: Tracker.AngularVelocitiesGlobalToDevice +.. py:method:: DEMVisualizer.Initialize .. code-block:: text - AngularVelocitiesGlobalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + Initialize(self: deme._deme.DEMVisualizer) -> None - Synchronously copy all tracked global-frame angular velocities to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + Create the visualization window and graphics resources. -.. py:method:: Tracker.AngularVelocitiesLocal +.. py:method:: DEMVisualizer.IsPaused .. code-block:: text - AngularVelocitiesLocal(self: deme._deme.Tracker) -> list[list[float]] - - Return local principal-axis-frame angular velocities for all tracked owners. + IsPaused(self: deme._deme.DEMVisualizer) -> bool -.. py:method:: Tracker.AngularVelocitiesLocalToDevice +.. py:method:: DEMVisualizer.IsRenderingSpheres .. code-block:: text - AngularVelocitiesLocalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None - - Synchronously copy all tracked local-frame angular velocities to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + IsRenderingSpheres(self: deme._deme.DEMVisualizer) -> bool -.. py:method:: Tracker.AngVelGlobal +.. py:method:: DEMVisualizer.IsRenderingTriangles .. code-block:: text - AngVelGlobal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] - - Return the global-frame angular velocity of the owner at ``offset``. + IsRenderingTriangles(self: deme._deme.DEMVisualizer) -> bool -.. py:method:: Tracker.AngVelLocal +.. py:method:: DEMVisualizer.IsSelectedSphere .. code-block:: text - AngVelLocal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] - - Return the angular velocity of the owner at ``offset`` in its local principal-axis frame. Rotate it by ``OriQ(offset)`` to obtain the global-frame value. + IsSelectedSphere(self: deme._deme.DEMVisualizer) -> bool -.. py:method:: Tracker.ContactAcc +.. py:method:: DEMVisualizer.PickAt .. code-block:: text - ContactAcc(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] - - Get the a portion of the acceleration of this tracked object, that is the result of its contact with other simulation entities. In most cases, this means excluding the gravitational acceleration. The acceleration is in global frame. + PickAt(self: deme._deme.DEMVisualizer, x: typing.SupportsInt | typing.SupportsIndex, y: typing.SupportsInt | typing.SupportsIndex) -> None -.. py:method:: Tracker.ContactAccelerations +.. py:method:: DEMVisualizer.Render .. code-block:: text - ContactAccelerations(self: deme._deme.Tracker) -> list[list[float]] + Render(self: deme._deme.DEMVisualizer) -> None - Get the acceleration experienced by all objects tracked by this tracker, that is the result of their contact with other simulation entities. The acceleration is in global frame. In most cases, this means excluding the gravitational acceleration. The acceleration is in global frame. + Synchronously capture the current solver state and draw one frame without advancing the solver. -.. py:method:: Tracker.ContactAccelerationsToDevice +.. py:method:: DEMVisualizer.RequestScreenshot .. code-block:: text - ContactAccelerationsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None - - Synchronously copy all tracked global-frame contact accelerations to caller-owned CUDA ``float3`` storage. Gravity and other non-contact acceleration are excluded. + RequestScreenshot(self: deme._deme.DEMVisualizer, path: str) -> None -.. py:method:: Tracker.ContactAngAccGlobal +.. py:method:: DEMVisualizer.RequestStep .. code-block:: text - ContactAngAccGlobal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] - - Get the a portion of the angular acceleration of this tracked object, that is the result of its contact with other simulation entities. The acceleration is in this object's global frame. + RequestStep(self: deme._deme.DEMVisualizer) -> None -.. py:method:: Tracker.ContactAngAccLocal +.. py:method:: DEMVisualizer.Run .. code-block:: text - ContactAngAccLocal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + Run(self: deme._deme.DEMVisualizer) -> bool - Get the a portion of the angular acceleration of this tracked object, that is the result of its contact with other simulation entities. The acceleration is in this object's local frame. + Return true while the initialized window remains open. -.. py:method:: Tracker.ContactAngularAccelerationsGlobal +.. py:method:: DEMVisualizer.SetBackgroundColor .. code-block:: text - ContactAngularAccelerationsGlobal(self: deme._deme.Tracker) -> list[list[float]] - - Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of their contact with other simulation entities. The acceleration is in this object's global frame. + SetBackgroundColor(self: deme._deme.DEMVisualizer, color: deme._deme.VisualizerColor) -> None -.. py:method:: Tracker.ContactAngularAccelerationsGlobalToDevice +.. py:method:: DEMVisualizer.SetCameraPosition .. code-block:: text - ContactAngularAccelerationsGlobalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None - - Synchronously copy all tracked global-frame contact angular accelerations to caller-owned CUDA ``float3`` storage. + SetCameraPosition(self: deme._deme.DEMVisualizer, position: float3) -> None -.. py:method:: Tracker.ContactAngularAccelerationsLocal +.. py:method:: DEMVisualizer.SetCameraTarget .. code-block:: text - ContactAngularAccelerationsLocal(self: deme._deme.Tracker) -> list[list[float]] - - Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of their contact with other simulation entities. The acceleration is in this object's local frame. + SetCameraTarget(self: deme._deme.DEMVisualizer, target: float3) -> None -.. py:method:: Tracker.ContactAngularAccelerationsLocalToDevice +.. py:method:: DEMVisualizer.SetColorMode .. code-block:: text - ContactAngularAccelerationsLocalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None - - Synchronously copy all tracked local-frame contact angular accelerations to caller-owned CUDA ``float3`` storage. + SetColorMode(self: deme._deme.DEMVisualizer, mode: deme._deme.VisualizerColorMode) -> None -.. py:method:: Tracker.FamiliesToDevice +.. py:method:: DEMVisualizer.SetFamilyColor .. code-block:: text - FamiliesToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None - - Synchronously copy all tracked family numbers to caller-owned CUDA ``uint32`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + SetFamilyColor(self: deme._deme.DEMVisualizer, family: typing.SupportsInt | typing.SupportsIndex, color: deme._deme.VisualizerColor) -> None -.. py:method:: Tracker.GetContactClumps +.. py:method:: DEMVisualizer.SetFamilyVisible .. code-block:: text - GetContactClumps(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[int] - - Get the clumps that are in contact with this tracked owner as a vector. + SetFamilyVisible(self: deme._deme.DEMVisualizer, family: typing.SupportsInt | typing.SupportsIndex, visible: bool) -> None -.. py:method:: Tracker.GetContactForces +.. py:method:: DEMVisualizer.SetPaused .. code-block:: text - GetContactForces(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int - - Get all contact forces that concern this tracked object. Returns number of force pairs. + SetPaused(self: deme._deme.DEMVisualizer, paused: bool) -> None -.. py:method:: Tracker.GetContactForcesAndGlobalTorque +.. py:method:: DEMVisualizer.SetRenderSpheres .. code-block:: text - GetContactForcesAndGlobalTorque(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3], offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + SetRenderSpheres(self: deme._deme.DEMVisualizer, render: bool = True) -> None - Get all contact forces and global torques that concern this tracked object. Returns number of force pairs. + Enable or disable component-sphere rendering; enabled by default. -.. py:method:: Tracker.GetContactForcesAndGlobalTorqueForAll +.. py:method:: DEMVisualizer.SetRenderTriangles .. code-block:: text - GetContactForcesAndGlobalTorqueForAll(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3]) -> int + SetRenderTriangles(self: deme._deme.DEMVisualizer, render: bool = True) -> None - Get all contact forces and global torques that concern all objects tracked by this tracker. Returns number of force pairs. + Enable or disable triangle rendering; enabled by default. -.. py:method:: Tracker.GetContactForcesAndGlobalTorqueForAllToDevice +.. py:method:: DEMVisualizer.SetTargetFPS .. code-block:: text - GetContactForcesAndGlobalTorqueForAllToDevice(self: deme._deme.Tracker, points: typing.SupportsInt | typing.SupportsIndex, forces: typing.SupportsInt | typing.SupportsIndex, torques: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> int - - Synchronously compact contact points, forces, and global-frame extra torques into caller-owned CUDA ``float3`` arrays. Capacity must cover the total recorded-contact count; the return value is the number of valid rows. + SetTargetFPS(self: deme._deme.DEMVisualizer, fps: typing.SupportsInt | typing.SupportsIndex) -> None -.. py:method:: Tracker.GetContactForcesAndLocalTorque +.. py:method:: DEMVisualizer.SetWindowSize .. code-block:: text - GetContactForcesAndLocalTorque(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3], offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int - - Get all contact forces and local torques that concern this tracked object. Returns number of force pairs. + SetWindowSize(self: deme._deme.DEMVisualizer, width: typing.SupportsInt | typing.SupportsIndex, height: typing.SupportsInt | typing.SupportsIndex) -> None -.. py:method:: Tracker.GetContactForcesAndLocalTorqueForAll +.. py:method:: DEMVisualizer.SetWindowTitle .. code-block:: text - GetContactForcesAndLocalTorqueForAll(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3]) -> int - - Get all contact forces and local torques that concern all objects tracked by this tracker. Returns number of force pairs. + SetWindowTitle(self: deme._deme.DEMVisualizer, title: str) -> None -.. py:method:: Tracker.GetContactForcesAndLocalTorqueForAllToDevice +.. py:method:: DEMVisualizer.ShouldStep .. code-block:: text - GetContactForcesAndLocalTorqueForAllToDevice(self: deme._deme.Tracker, points: typing.SupportsInt | typing.SupportsIndex, forces: typing.SupportsInt | typing.SupportsIndex, torques: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> int + ShouldStep(self: deme._deme.DEMVisualizer) -> bool - Synchronously compact contact points, forces, and owner-local extra torques into caller-owned CUDA ``float3`` arrays. Capacity must cover the total recorded-contact count; the return value is the number of valid rows. -.. py:method:: Tracker.GetContactForcesForAll +CNT_OUTPUT_CONTENT +------------------ + +.. py:class:: CNT_OUTPUT_CONTENT .. code-block:: text - GetContactForcesForAll(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3]) -> int + Members: - Get all contact forces that concern all objects tracked by this tracker. Returns number of force pairs. + CNT_TYPE + FORCE -.. py:method:: Tracker.GetContactForcesForAllToDevice + POINT - .. code-block:: text + COMPONENT - GetContactForcesForAllToDevice(self: deme._deme.Tracker, points: typing.SupportsInt | typing.SupportsIndex, forces: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> int + NORMAL + TORQUE - Synchronously compact contact points and forces for all tracked owners into CUDA ``float3`` arrays. + CNT_WILDCARD - Each pointer must address ``capacity`` elements on logical CUDA ``device``. - Capacity must cover the simulation's total recorded-contact count. The return - value is the number of valid compacted rows. Contact force recording must be - enabled. + OWNER + GEO_ID -.. py:method:: Tracker.GetFamilies + NICKNAME - .. code-block:: text +.. py:method:: CNT_OUTPUT_CONTENT.__init__ - GetFamilies(self: deme._deme.Tracker) -> list[int] + .. code-block:: text - Get the family numbers of all the tracked object. + __init__(self: deme._deme.CNT_OUTPUT_CONTENT, value: typing.SupportsInt | typing.SupportsIndex) -> None -.. py:method:: Tracker.GetFamily +.. py:attribute:: CNT_OUTPUT_CONTENT.CNT_TYPE .. code-block:: text - GetFamily(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + Members: - Get the family number of the tracked object. + CNT_TYPE + FORCE -.. py:method:: Tracker.GetMesh + POINT - .. code-block:: text + COMPONENT - GetMesh(self: deme._deme.Tracker) -> deme::DEMMesh + NORMAL - Get a handle for the mesh this tracker is tracking. + TORQUE + CNT_WILDCARD -.. py:method:: Tracker.GetMeshNodesGlobal + OWNER - .. code-block:: text + GEO_ID - GetMeshNodesGlobal(self: deme._deme.Tracker) -> list[float3] + NICKNAME - Get the current locations of all the nodes in the mesh being tracked. +.. py:attribute:: CNT_OUTPUT_CONTENT.CNT_WILDCARD + .. code-block:: text -.. py:method:: Tracker.GetOwnerID + Members: - .. code-block:: text + CNT_TYPE - GetOwnerID(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + FORCE - Return the simulation-wide owner ID at ``offset`` within this tracker. + POINT + COMPONENT -.. py:method:: Tracker.GetOwnerIDs + NORMAL - .. code-block:: text + TORQUE - GetOwnerIDs(self: deme._deme.Tracker) -> list[int] + CNT_WILDCARD - Return simulation-wide owner IDs for every owner covered by this tracker. + OWNER + GEO_ID -.. py:method:: Tracker.GetOwnerWildcardValue + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.COMPONENT .. code-block:: text - GetOwnerWildcardValue(self: deme._deme.Tracker, name: str, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> float + Members: - Get the owner's wildcard value. + CNT_TYPE + FORCE -.. py:method:: Tracker.GetOwnerWildcardValues + POINT - .. code-block:: text + COMPONENT - GetOwnerWildcardValues(self: deme._deme.Tracker, name: str) -> list[float] + NORMAL - Get the owner wildcard values for all the owners entities tracked by this tracker. + TORQUE + + CNT_WILDCARD + OWNER -.. py:method:: Tracker.Mass + GEO_ID - .. code-block:: text + NICKNAME - Mass(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> float +.. py:attribute:: CNT_OUTPUT_CONTENT.FORCE - Get the mass of the tracked object. + .. code-block:: text + Members: -.. py:method:: Tracker.Masses + CNT_TYPE - .. code-block:: text + FORCE - Masses(self: deme._deme.Tracker) -> list[float] + POINT - Get the masses of all the tracked objects. + COMPONENT + NORMAL -.. py:method:: Tracker.MassesToDevice + TORQUE - .. code-block:: text + CNT_WILDCARD - MassesToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + OWNER - Synchronously copy all tracked masses to caller-owned CUDA ``float32`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + GEO_ID + NICKNAME -.. py:method:: Tracker.MOI +.. py:attribute:: CNT_OUTPUT_CONTENT.GEO_ID .. code-block:: text - MOI(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + Members: - Get the moment of inertia (in principal axis frame) of the tracked object. + CNT_TYPE + FORCE -.. py:method:: Tracker.MOIs + POINT - .. code-block:: text + COMPONENT - MOIs(self: deme._deme.Tracker) -> list[list[float]] + NORMAL - Get the moment of inertia (in principal axis frame) of all the tracked objects. + TORQUE + CNT_WILDCARD -.. py:method:: Tracker.MOIsToDevice + OWNER - .. code-block:: text + GEO_ID - MOIsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + NICKNAME - Synchronously copy all tracked principal moments of inertia to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. +.. py:attribute:: CNT_OUTPUT_CONTENT.NICKNAME + .. code-block:: text -.. py:method:: Tracker.OrientationQuaternions + Members: - .. code-block:: text + CNT_TYPE - OrientationQuaternions(self: deme._deme.Tracker) -> list[list[float]] + FORCE - Return local-to-global orientation quaternions for all tracked owners. Each quaternion uses Python ordering ``(x, y, z, w)``. + POINT + COMPONENT -.. py:method:: Tracker.OrientationQuaternionsToDevice + NORMAL - .. code-block:: text + TORQUE - OrientationQuaternionsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + CNT_WILDCARD - Synchronously copy all tracked local-to-global quaternions to caller-owned CUDA ``float4`` storage in ``(x, y, z, w)`` order. ``capacity`` is in elements. + OWNER + GEO_ID -.. py:method:: Tracker.OriQ + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.NORMAL .. code-block:: text - OriQ(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + Members: - Return the quaternion rotating the owner-local frame to the global frame. Python ordering is ``(x, y, z, w)`` (Chrono ``(e1, e2, e3, e0)``). + CNT_TYPE + FORCE -.. py:method:: Tracker.OwnerWildcardValuesToDevice + POINT - .. code-block:: text + COMPONENT - OwnerWildcardValuesToDevice(self: deme._deme.Tracker, name: str, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + NORMAL - Synchronously copy the named ``float32`` owner wildcard for every tracked owner to caller-owned CUDA storage. ``capacity`` is in owner elements. + TORQUE + CNT_WILDCARD -.. py:method:: Tracker.Pos + OWNER - .. code-block:: text + GEO_ID - Pos(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + NICKNAME - Return the global-frame center-of-mass position of the tracked owner at ``offset``. +.. py:attribute:: CNT_OUTPUT_CONTENT.OWNER + .. code-block:: text -.. py:method:: Tracker.Positions - - .. code-block:: text + Members: - Positions(self: deme._deme.Tracker) -> list[list[float]] + CNT_TYPE - Return global-frame center-of-mass positions for all owners covered by this tracker. + FORCE + POINT -.. py:method:: Tracker.PositionsToDevice + COMPONENT - .. code-block:: text + NORMAL - PositionsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + TORQUE + CNT_WILDCARD - Synchronously copy all tracked global positions to caller-owned CUDA memory. + OWNER - ``pointer`` is an integer address to writable, CUDA-accessible ``float3`` - storage on logical CUDA ``device``. ``capacity`` is measured in ``float3`` - elements and must cover every owner in this tracker. The caller owns the - allocation and must keep it alive through the call. + GEO_ID + NICKNAME -.. py:method:: Tracker.SetAngVel +.. py:attribute:: CNT_OUTPUT_CONTENT.POINT .. code-block:: text - SetAngVel(*args, **kwargs) - Overloaded function. - - 1. SetAngVel(self: deme._deme.Tracker, angVel: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None - - Set the angular velocity of this tracked object in its own local coordinate system. - - 2. SetAngVel(self: deme._deme.Tracker, angVel: collections.abc.Sequence[float3]) -> None + Members: - Set the angular velocity of consecutive tracked objects in their own local coordinate systems. + CNT_TYPE + FORCE -.. py:method:: Tracker.SetFamily + POINT - .. code-block:: text + COMPONENT - SetFamily(*args, **kwargs) - Overloaded function. + NORMAL - 1. SetFamily(self: deme._deme.Tracker, fam_num: typing.SupportsInt | typing.SupportsIndex) -> None + TORQUE - Change the family numbers of all the entities tracked by this tracker. + CNT_WILDCARD - 2. SetFamily(self: deme._deme.Tracker, fam_num: typing.SupportsInt | typing.SupportsIndex, offset: typing.SupportsInt | typing.SupportsIndex) -> None + OWNER - Change the family number of one entities tracked by this tracker. + GEO_ID + NICKNAME -.. py:method:: Tracker.SetOriQ +.. py:attribute:: CNT_OUTPUT_CONTENT.TORQUE .. code-block:: text - SetOriQ(*args, **kwargs) - Overloaded function. + Members: - 1. SetOriQ(self: deme._deme.Tracker, oriQ: float4, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + CNT_TYPE - Set the quaternion which represents the orientation of this tracked object's coordinate system. + FORCE - 2. SetOriQ(self: deme._deme.Tracker, oriQ: collections.abc.Sequence[float4]) -> None + POINT - Set the quaternion which represents the orientation of consecutive tracked objects' coordinate systems. + COMPONENT + NORMAL -.. py:method:: Tracker.SetOwnerWildcardValue + TORQUE - .. code-block:: text + CNT_WILDCARD - SetOwnerWildcardValue(self: deme._deme.Tracker, name: str, wc: typing.SupportsFloat | typing.SupportsIndex, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + OWNER - Set a wildcard value of the owner this tracker is tracking. + GEO_ID + NICKNAME -.. py:method:: Tracker.SetOwnerWildcardValues - .. code-block:: text +ContactInfoContainer +-------------------- - SetOwnerWildcardValues(self: deme._deme.Tracker, name: str, wc: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None +.. py:class:: ContactInfoContainer - Set a wildcard value of the owner this tracker is tracking. + .. code-block:: text + Named arrays describing contacts selected by ``DEMSolver.SetContactOutputContent``. -.. py:method:: Tracker.SetPos +.. py:method:: ContactInfoContainer.__init__ .. code-block:: text - SetPos(*args, **kwargs) - Overloaded function. + __init__(self: deme._deme.ContactInfoContainer, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: collections.abc.Sequence[tuple[str, str]]) -> None - 1. SetPos(self: deme._deme.Tracker, pos: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None - Set the position of this tracked object. +.. py:method:: ContactInfoContainer.GetAGeo - 2. SetPos(self: deme._deme.Tracker, pos: collections.abc.Sequence[float3]) -> None + .. code-block:: text - Set the positions of consecutive tracked objects. + GetAGeo(self: deme._deme.ContactInfoContainer) -> list[int] + Get AGeo number from the container. -.. py:method:: Tracker.SetVel + +.. py:method:: ContactInfoContainer.GetAOwner .. code-block:: text - SetVel(*args, **kwargs) - Overloaded function. + GetAOwner(self: deme._deme.ContactInfoContainer) -> list[int] - 1. SetVel(self: deme._deme.Tracker, vel: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + Get AOwner number from the container. - Set the velocity of this tracked object in global frame. - 2. SetVel(self: deme._deme.Tracker, vel: collections.abc.Sequence[float3]) -> None +.. py:method:: ContactInfoContainer.GetAOwnerFamily - Set the velocity of consecutive tracked objects in global frame. + .. code-block:: text + GetAOwnerFamily(self: deme._deme.ContactInfoContainer) -> list[int] -.. py:method:: Tracker.UpdateMesh + Get AOwnerFamily number from the container. + + +.. py:method:: ContactInfoContainer.GetBGeo .. code-block:: text - UpdateMesh(self: deme._deme.Tracker, new_nodes: collections.abc.Sequence[float3]) -> None + GetBGeo(self: deme._deme.ContactInfoContainer) -> list[int] - Apply the new mesh node positions such that the tracked mesh is replaced by the new_nodes. + Get BGeo number from the container. -.. py:method:: Tracker.UpdateMeshByIncrement +.. py:method:: ContactInfoContainer.GetBOwner .. code-block:: text - UpdateMeshByIncrement(self: deme._deme.Tracker, deformation: collections.abc.Sequence[float3]) -> None + GetBOwner(self: deme._deme.ContactInfoContainer) -> list[int] - Change the coordinates of each mesh node by the given amount. + Get BOwner number from the container. -.. py:method:: Tracker.Vel +.. py:method:: ContactInfoContainer.GetBOwnerFamily .. code-block:: text - Vel(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + GetBOwnerFamily(self: deme._deme.ContactInfoContainer) -> list[int] - Return the global-frame linear velocity of the owner at ``offset``. + Get BOwnerFamily number from the container. -.. py:method:: Tracker.Velocities +.. py:method:: ContactInfoContainer.GetContactType .. code-block:: text - Velocities(self: deme._deme.Tracker) -> list[list[float]] + GetContactType(self: deme._deme.ContactInfoContainer) -> list[str] - Return global-frame linear velocities for all tracked owners. + Get contact type as strings from the container. -.. py:method:: Tracker.VelocitiesToDevice +.. py:method:: ContactInfoContainer.GetForce .. code-block:: text - VelocitiesToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None - - Synchronously copy all tracked global linear velocities to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. - + GetForce(self: deme._deme.ContactInfoContainer) -> list[float3] + Get force as vectors from the container. -CNT_OUTPUT_CONTENT ------------------- -.. py:class:: CNT_OUTPUT_CONTENT +.. py:method:: ContactInfoContainer.GetNormal .. code-block:: text - Members: + GetNormal(self: deme._deme.ContactInfoContainer) -> list[float3] - CNT_TYPE + Get contact normal as vectors from the container. - FORCE - POINT +.. py:method:: ContactInfoContainer.GetPoint - COMPONENT + .. code-block:: text - NORMAL + GetPoint(self: deme._deme.ContactInfoContainer) -> list[float3] - TORQUE + Get contact points as vectors from the container. - CNT_WILDCARD - OWNER +.. py:method:: ContactInfoContainer.GetTorque - GEO_ID + .. code-block:: text - NICKNAME + GetTorque(self: deme._deme.ContactInfoContainer) -> list[float3] -.. py:method:: CNT_OUTPUT_CONTENT.__init__ + Get torque as vectors from the container. - .. code-block:: text - __init__(self: deme._deme.CNT_OUTPUT_CONTENT, value: typing.SupportsInt | typing.SupportsIndex) -> None +DataContainer +------------- -.. py:attribute:: CNT_OUTPUT_CONTENT.CNT_TYPE +.. py:class:: DataContainer .. code-block:: text - Members: + String-keyed container used to return named arrays from DEME. - CNT_TYPE +.. py:method:: DataContainer.__init__ - FORCE + .. code-block:: text - POINT + __init__(self: deme._deme.DataContainer) -> None - COMPONENT + Create an empty data container. - NORMAL - TORQUE +.. py:method:: DataContainer.Get - CNT_WILDCARD + .. code-block:: text - OWNER + Get(self: deme._deme.DataContainer, key: str) -> list[float] - GEO_ID + Return the stored float array named ``key`` by reference. This is primarily used for wildcard output. - NICKNAME -.. py:attribute:: CNT_OUTPUT_CONTENT.CNT_WILDCARD + +DEMClumpBatch +------------- + +.. py:class:: DEMClumpBatch .. code-block:: text - Members: + Cached setup data for a batch of clump instances. - CNT_TYPE +.. py:method:: DEMClumpBatch.__init__ - FORCE + .. code-block:: text - POINT + __init__(self: deme._deme.DEMClumpBatch, count: typing.SupportsInt | typing.SupportsIndex) -> None - COMPONENT + Create setup storage for ``count`` clumps. - NORMAL - TORQUE +.. py:method:: DEMClumpBatch.AddExistingContactWildcard - CNT_WILDCARD + .. code-block:: text - OWNER + AddExistingContactWildcard(self: deme._deme.DEMClumpBatch, name: str, values: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - GEO_ID + Add one named restart contact-wildcard array after ``SetExistingContacts``. - NICKNAME -.. py:attribute:: CNT_OUTPUT_CONTENT.COMPONENT +.. py:method:: DEMClumpBatch.AddOwnerWildcard .. code-block:: text - Members: + AddOwnerWildcard(*args, **kwargs) + Overloaded function. - CNT_TYPE + 1. AddOwnerWildcard(self: deme._deme.DEMClumpBatch, name: str, values: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - FORCE + Add one initial owner wildcard with one value per clump. - POINT + 2. AddOwnerWildcard(self: deme._deme.DEMClumpBatch, name: str, value: typing.SupportsFloat | typing.SupportsIndex) -> None - COMPONENT + Add one initial owner wildcard with the same value for every clump. - NORMAL - TORQUE +.. py:method:: DEMClumpBatch.GetNumClumps - CNT_WILDCARD + .. code-block:: text - OWNER + GetNumClumps(self: deme._deme.DEMClumpBatch) -> int - GEO_ID + Return the number of clumps in this batch. - NICKNAME -.. py:attribute:: CNT_OUTPUT_CONTENT.FORCE +.. py:method:: DEMClumpBatch.GetNumContacts .. code-block:: text - Members: + GetNumContacts(self: deme._deme.DEMClumpBatch) -> int - CNT_TYPE + Return the number of pre-existing contacts supplied for restart initialization. - FORCE - POINT +.. py:method:: DEMClumpBatch.GetNumSpheres - COMPONENT + .. code-block:: text - NORMAL + GetNumSpheres(self: deme._deme.DEMClumpBatch) -> int - TORQUE - - CNT_WILDCARD + Return the total number of component spheres represented by this batch. - OWNER - GEO_ID +.. py:method:: DEMClumpBatch.SetExistingContacts - NICKNAME + .. code-block:: text -.. py:attribute:: CNT_OUTPUT_CONTENT.GEO_ID + SetExistingContacts(self: deme._deme.DEMClumpBatch, pairs: collections.abc.Sequence[tuple[typing.SupportsInt | typing.SupportsIndex, typing.SupportsInt | typing.SupportsIndex]]) -> None - .. code-block:: text + Supply sphere-sphere contact owner-ID pairs relative to this batch when restoring a simulation. - Members: - CNT_TYPE +.. py:method:: DEMClumpBatch.SetExistingContactWildcards - FORCE + .. code-block:: text - POINT + SetExistingContactWildcards(self: deme._deme.DEMClumpBatch, wildcards: collections.abc.Mapping[str, collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> None - COMPONENT + Supply named restart wildcard arrays after ``SetExistingContacts``. Every array must match the pre-existing contact count. - NORMAL - TORQUE +.. py:method:: DEMClumpBatch.SetFamilies - CNT_WILDCARD + .. code-block:: text - OWNER + SetFamilies(*args, **kwargs) + Overloaded function. - GEO_ID + 1. SetFamilies(self: deme._deme.DEMClumpBatch, families: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> None - NICKNAME + Set one family number per clump. Family behavior is configured on the solver. -.. py:attribute:: CNT_OUTPUT_CONTENT.NICKNAME + 2. SetFamilies(self: deme._deme.DEMClumpBatch, family: typing.SupportsInt | typing.SupportsIndex) -> None - .. code-block:: text + Set the same family number for every clump. - Members: - CNT_TYPE +.. py:method:: DEMClumpBatch.SetFamily - FORCE + .. code-block:: text - POINT + SetFamily(self: deme._deme.DEMClumpBatch, family: typing.SupportsInt | typing.SupportsIndex) -> None - COMPONENT + Set the same family number for every clump. - NORMAL - TORQUE +.. py:method:: DEMClumpBatch.SetOwnerWildcards - CNT_WILDCARD + .. code-block:: text - OWNER + SetOwnerWildcards(self: deme._deme.DEMClumpBatch, wildcards: collections.abc.Mapping[str, collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> None - GEO_ID + Set named initial owner-wildcard arrays. Every array must match the clump count. - NICKNAME -.. py:attribute:: CNT_OUTPUT_CONTENT.NORMAL +.. py:method:: DEMClumpBatch.SetType .. code-block:: text - Members: + SetType(self: deme._deme.DEMClumpBatch, type: deme._deme.DEMClumpTemplate) -> None - CNT_TYPE + Assign the same clump template to every instance. - FORCE - POINT +.. py:method:: DEMClumpBatch.SetTypes - COMPONENT + .. code-block:: text - NORMAL + SetTypes(*args, **kwargs) + Overloaded function. - TORQUE + 1. SetTypes(self: deme._deme.DEMClumpBatch, types: collections.abc.Sequence[deme._deme.DEMClumpTemplate]) -> None - CNT_WILDCARD + Assign one clump template per instance. The sequence length must equal the batch size. - OWNER + 2. SetTypes(self: deme._deme.DEMClumpBatch, type: deme._deme.DEMClumpTemplate) -> None - GEO_ID + Assign the same clump template to every instance. - NICKNAME -.. py:attribute:: CNT_OUTPUT_CONTENT.OWNER +.. py:method:: DEMClumpBatch.SetVel .. code-block:: text - Members: - - CNT_TYPE - - FORCE - - POINT + SetVel(*args, **kwargs) + Overloaded function. - COMPONENT + 1. SetVel(self: deme._deme.DEMClumpBatch, velocities: collections.abc.Sequence[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> None - NORMAL + Set one global initial linear velocity vector per clump. - TORQUE + 2. SetVel(self: deme._deme.DEMClumpBatch, velocity: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - CNT_WILDCARD + Set the same global initial linear velocity for every clump. - OWNER - GEO_ID - NICKNAME +DEMClumpTemplate +---------------- -.. py:attribute:: CNT_OUTPUT_CONTENT.POINT +.. py:class:: DEMClumpTemplate .. code-block:: text - Members: + Reusable rigid clump topology composed of one or more sphere components. - CNT_TYPE +.. py:method:: DEMClumpTemplate.__init__ - FORCE + .. code-block:: text - POINT + __init__(self: deme._deme.DEMClumpTemplate) -> None - COMPONENT + Create an empty clump template. - NORMAL - TORQUE +.. py:method:: DEMClumpTemplate.AssignName - CNT_WILDCARD + .. code-block:: text - OWNER + AssignName(self: deme._deme.DEMClumpTemplate, name: str) -> None - GEO_ID + Assign the clump type name written to output files. - NICKNAME -.. py:attribute:: CNT_OUTPUT_CONTENT.TORQUE +.. py:method:: DEMClumpTemplate.InformCentroidPrincipal .. code-block:: text - Members: + InformCentroidPrincipal(self: deme._deme.DEMClumpTemplate, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], orientation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - CNT_TYPE + Re-express component positions about the supplied center of mass and principal orientation. - FORCE - POINT +.. py:method:: DEMClumpTemplate.Mass - COMPONENT + .. code-block:: text - NORMAL + Mass(self: deme._deme.DEMClumpTemplate) -> float - TORQUE + Return the clump mass. - CNT_WILDCARD - OWNER +.. py:method:: DEMClumpTemplate.MOI - GEO_ID + .. code-block:: text - NICKNAME + MOI(self: deme._deme.DEMClumpTemplate) -> float3 + Return principal moments of inertia in the clump-local principal frame. -ContactInfoContainer --------------------- -.. py:class:: ContactInfoContainer +.. py:method:: DEMClumpTemplate.Move .. code-block:: text - Named arrays describing contacts selected by ``DEMSolver.SetContactOutputContent``. - -.. py:method:: ContactInfoContainer.__init__ - - .. code-block:: text + Move(self: deme._deme.DEMClumpTemplate, translation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - __init__(self: deme._deme.ContactInfoContainer, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: collections.abc.Sequence[tuple[str, str]]) -> None + Apply a translation and quaternion rotation to the component coordinates. -.. py:method:: ContactInfoContainer.GetAGeo +.. py:method:: DEMClumpTemplate.ReadComponentFromFile .. code-block:: text - GetAGeo(self: deme._deme.ContactInfoContainer) -> list[int] + ReadComponentFromFile(self: deme._deme.DEMClumpTemplate, filename: str, x_id: str = 'x', y_id: str = 'y', z_id: str = 'z', r_id: str = 'r') -> int - Get AGeo number from the container. + Load sphere-component positions and radii from a delimited file. -.. py:method:: ContactInfoContainer.GetAOwner +.. py:method:: DEMClumpTemplate.Scale .. code-block:: text - GetAOwner(self: deme._deme.ContactInfoContainer) -> list[int] + Scale(self: deme._deme.DEMClumpTemplate, factor: typing.SupportsFloat | typing.SupportsIndex) -> None - Get AOwner number from the container. + Uniformly scale component radii and relative positions. -.. py:method:: ContactInfoContainer.GetAOwnerFamily +.. py:method:: DEMClumpTemplate.SetMass .. code-block:: text - GetAOwnerFamily(self: deme._deme.ContactInfoContainer) -> list[int] + SetMass(self: deme._deme.DEMClumpTemplate, mass: typing.SupportsFloat | typing.SupportsIndex) -> None - Get AOwnerFamily number from the container. + Set the clump mass. -.. py:method:: ContactInfoContainer.GetBGeo +.. py:method:: DEMClumpTemplate.SetMaterial .. code-block:: text - GetBGeo(self: deme._deme.ContactInfoContainer) -> list[int] - - Get BGeo number from the container. - + SetMaterial(*args, **kwargs) + Overloaded function. -.. py:method:: ContactInfoContainer.GetBOwner + 1. SetMaterial(self: deme._deme.DEMClumpTemplate, materials: collections.abc.Sequence[deme._deme.DEMMaterial]) -> None - .. code-block:: text + Assign one material per sphere component. The sequence length must equal the component count. - GetBOwner(self: deme._deme.ContactInfoContainer) -> list[int] + 2. SetMaterial(self: deme._deme.DEMClumpTemplate, material: deme._deme.DEMMaterial) -> None - Get BOwner number from the container. + Assign the same material to every sphere component. -.. py:method:: ContactInfoContainer.GetBOwnerFamily +.. py:method:: DEMClumpTemplate.SetMOI .. code-block:: text - GetBOwnerFamily(self: deme._deme.ContactInfoContainer) -> list[int] + SetMOI(self: deme._deme.DEMClumpTemplate, MOI: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - Get BOwnerFamily number from the container. + Set the three principal moments of inertia in the clump-local frame. -.. py:method:: ContactInfoContainer.GetContactType +.. py:method:: DEMClumpTemplate.SetVolume .. code-block:: text - GetContactType(self: deme._deme.ContactInfoContainer) -> list[str] + SetVolume(self: deme._deme.DEMClumpTemplate, volume: typing.SupportsFloat | typing.SupportsIndex) -> None - Get contact type as strings from the container. + Set the clump volume, which is required for void-ratio queries. -.. py:method:: ContactInfoContainer.GetForce - .. code-block:: text +DEMCombinedInstances +-------------------- - GetForce(self: deme._deme.ContactInfoContainer) -> list[float3] +.. py:class:: DEMCombinedInstances - Get force as vectors from the container. + .. code-block:: text + A batch of instantiated combined owners and their member-owner metadata. -.. py:method:: ContactInfoContainer.GetNormal +.. py:method:: DEMCombinedInstances.__init__ .. code-block:: text - GetNormal(self: deme._deme.ContactInfoContainer) -> list[float3] + __init__(self: deme._deme.DEMCombinedInstances) -> None - Get contact normal as vectors from the container. + Create an empty combined-instance batch. -.. py:method:: ContactInfoContainer.GetPoint +.. py:method:: DEMCombinedInstances.AddOwnerWildcard .. code-block:: text - GetPoint(self: deme._deme.ContactInfoContainer) -> list[float3] + AddOwnerWildcard(*args, **kwargs) + Overloaded function. - Get contact points as vectors from the container. + 1. AddOwnerWildcard(self: deme._deme.DEMCombinedInstances, name: str, vals: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + Add an owner wildcard to all member owners with per-owner values. -.. py:method:: ContactInfoContainer.GetTorque + 2. AddOwnerWildcard(self: deme._deme.DEMCombinedInstances, name: str, val: typing.SupportsFloat | typing.SupportsIndex) -> None + + Add an owner wildcard to all member owners with a uniform value. + + +.. py:method:: DEMCombinedInstances.GetNumOwners .. code-block:: text - GetTorque(self: deme._deme.ContactInfoContainer) -> list[float3] + GetNumOwners(self: deme._deme.DEMCombinedInstances) -> int - Get torque as vectors from the container. + Get total number of member owners in this combined batch. -DataContainer -------------- +DEMCombinedTemplate +------------------- -.. py:class:: DataContainer +.. py:class:: DEMCombinedTemplate .. code-block:: text - String-keyed container used to return named arrays from DEME. + Reusable rigid grouping of clump or mesh templates with fixed member-relative transforms. -.. py:method:: DataContainer.__init__ +.. py:method:: DEMCombinedTemplate.__init__ .. code-block:: text - __init__(self: deme._deme.DataContainer) -> None + __init__(self: deme._deme.DEMCombinedTemplate) -> None - Create an empty data container. + Create an empty combined-owner template. -.. py:method:: DataContainer.Get - .. code-block:: text - - Get(self: deme._deme.DataContainer, key: str) -> list[float] - - Return the stored float array named ``key`` by reference. This is primarily used for wildcard output. +DEMERuntimeDataHelper +--------------------- +.. py:class:: DEMERuntimeDataHelper + .. code-block:: text -DEMClumpBatch -------------- + No class description is currently available. -.. py:class:: DEMClumpBatch +.. py:method:: DEMERuntimeDataHelper.__init__ .. code-block:: text - Cached setup data for a batch of clump instances. + __init__(self: deme._deme.DEMERuntimeDataHelper) -> None -.. py:method:: DEMClumpBatch.__init__ + +.. py:method:: DEMERuntimeDataHelper.SetPathPrefix .. code-block:: text - __init__(self: deme._deme.DEMClumpBatch, count: typing.SupportsInt | typing.SupportsIndex) -> None + Convert a function to be a static method. - Create setup storage for ``count`` clumps. + A static method does not receive an implicit first argument. + To declare a static method, use this idiom: + class C: + @staticmethod + def f(arg1, arg2, argN): + ... -.. py:method:: DEMClumpBatch.AddExistingContactWildcard + It can be called either on the class (e.g. C.f()) or on an instance + (e.g. C().f()). Both the class and the instance are ignored, and + neither is passed implicitly as the first argument to the method. - .. code-block:: text + Static methods in Python are similar to those found in Java or C++. + For a more advanced concept, see the classmethod builtin. - AddExistingContactWildcard(self: deme._deme.DEMClumpBatch, name: str, values: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - Add one named restart contact-wildcard array after ``SetExistingContacts``. +DEMExternObj +------------ -.. py:method:: DEMClumpBatch.AddOwnerWildcard +.. py:class:: DEMExternObj .. code-block:: text - AddOwnerWildcard(*args, **kwargs) - Overloaded function. + Analytical external owner composed of planes, cylinders, and related primitives. - 1. AddOwnerWildcard(self: deme._deme.DEMClumpBatch, name: str, values: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None +.. py:method:: DEMExternObj.__init__ - Add one initial owner wildcard with one value per clump. + .. code-block:: text - 2. AddOwnerWildcard(self: deme._deme.DEMClumpBatch, name: str, value: typing.SupportsFloat | typing.SupportsIndex) -> None + __init__(self: deme._deme.DEMExternObj) -> None - Add one initial owner wildcard with the same value for every clump. + Create an empty analytical external object. -.. py:method:: DEMClumpBatch.GetNumClumps +.. py:method:: DEMExternObj.AddCone .. code-block:: text - GetNumClumps(self: deme._deme.DEMClumpBatch) -> int + AddCone(self: deme._deme.DEMExternObj, tip: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], axis: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], slope: typing.SupportsFloat | typing.SupportsIndex, material: deme._deme.DEMMaterial, normal: bool = False) -> None - Return the number of clumps in this batch. + Add an analytical cone side extending indefinitely from its tip. -.. py:method:: DEMClumpBatch.GetNumContacts +.. py:method:: DEMExternObj.AddConeSegment .. code-block:: text - GetNumContacts(self: deme._deme.DEMClumpBatch) -> int + AddConeSegment(self: deme._deme.DEMExternObj, tip: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], axis: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], slope: typing.SupportsFloat | typing.SupportsIndex, hmin: typing.SupportsFloat | typing.SupportsIndex, hmax: typing.SupportsFloat | typing.SupportsIndex, material: deme._deme.DEMMaterial, normal: bool = False) -> None - Return the number of pre-existing contacts supplied for restart initialization. + Add an analytical cone or frustum side clipped between two axial distances. -.. py:method:: DEMClumpBatch.GetNumSpheres +.. py:method:: DEMExternObj.AddCylinder .. code-block:: text - GetNumSpheres(self: deme._deme.DEMClumpBatch) -> int + AddCylinder(self: deme._deme.DEMExternObj, pos: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], axis: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], rad: typing.SupportsFloat | typing.SupportsIndex, material: deme._deme.DEMMaterial, normal: bool = False) -> None - Return the total number of component spheres represented by this batch. + Add an infinite cylinder along a user-specified object-local axis. -.. py:method:: DEMClumpBatch.SetExistingContacts +.. py:method:: DEMExternObj.AddPlane .. code-block:: text - SetExistingContacts(self: deme._deme.DEMClumpBatch, pairs: collections.abc.Sequence[tuple[typing.SupportsInt | typing.SupportsIndex, typing.SupportsInt | typing.SupportsIndex]]) -> None + AddPlane(self: deme._deme.DEMExternObj, point: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], normal: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], material: deme._deme.DEMMaterial) -> None - Supply sphere-sphere contact owner-ID pairs relative to this batch when restoring a simulation. + Add an infinite plane specified by a point and normal in the object's local frame. -.. py:method:: DEMClumpBatch.SetExistingContactWildcards +.. py:method:: DEMExternObj.AddZCylinder .. code-block:: text - SetExistingContactWildcards(self: deme._deme.DEMClumpBatch, wildcards: collections.abc.Mapping[str, collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> None + AddZCylinder(self: deme._deme.DEMExternObj, pos: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], rad: typing.SupportsFloat | typing.SupportsIndex, material: deme._deme.DEMMaterial, normal: bool = False) -> None - Supply named restart wildcard arrays after ``SetExistingContacts``. Every array must match the pre-existing contact count. + Add an infinite cylinder aligned with the object-local Z axis. -.. py:method:: DEMClumpBatch.SetFamilies +.. py:attribute:: DEMExternObj.entity_params .. code-block:: text - SetFamilies(*args, **kwargs) - Overloaded function. + Bound attribute. - 1. SetFamilies(self: deme._deme.DEMClumpBatch, families: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> None +.. py:attribute:: DEMExternObj.family_code - Set one family number per clump. Family behavior is configured on the solver. + .. code-block:: text - 2. SetFamilies(self: deme._deme.DEMClumpBatch, family: typing.SupportsInt | typing.SupportsIndex) -> None + Bound attribute. - Set the same family number for every clump. +.. py:attribute:: DEMExternObj.init_oriQ + .. code-block:: text -.. py:method:: DEMClumpBatch.SetFamily + Bound attribute. + +.. py:attribute:: DEMExternObj.init_pos .. code-block:: text - SetFamily(self: deme._deme.DEMClumpBatch, family: typing.SupportsInt | typing.SupportsIndex) -> None + Bound attribute. - Set the same family number for every clump. +.. py:attribute:: DEMExternObj.load_order + .. code-block:: text -.. py:method:: DEMClumpBatch.SetOwnerWildcards + Bound attribute. + +.. py:method:: DEMExternObj.Mass .. code-block:: text - SetOwnerWildcards(self: deme._deme.DEMClumpBatch, wildcards: collections.abc.Mapping[str, collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> None + Mass(self: deme._deme.DEMExternObj) -> float - Set named initial owner-wildcard arrays. Every array must match the clump count. + Return the object's mass. -.. py:method:: DEMClumpBatch.SetType +.. py:attribute:: DEMExternObj.mass .. code-block:: text - SetType(self: deme._deme.DEMClumpBatch, type: deme._deme.DEMClumpTemplate) -> None + Bound attribute. - Assign the same clump template to every instance. +.. py:attribute:: DEMExternObj.materials + .. code-block:: text -.. py:method:: DEMClumpBatch.SetTypes + Bound attribute. + +.. py:attribute:: DEMExternObj.MOI .. code-block:: text - SetTypes(*args, **kwargs) - Overloaded function. + Bound attribute. - 1. SetTypes(self: deme._deme.DEMClumpBatch, types: collections.abc.Sequence[deme._deme.DEMClumpTemplate]) -> None +.. py:method:: DEMExternObj.SetFamily - Assign one clump template per instance. The sequence length must equal the batch size. + .. code-block:: text - 2. SetTypes(self: deme._deme.DEMClumpBatch, type: deme._deme.DEMClumpTemplate) -> None + SetFamily(self: deme._deme.DEMExternObj, family: typing.SupportsInt | typing.SupportsIndex) -> None - Assign the same clump template to every instance. + Set the contact family number before initialization. -.. py:method:: DEMClumpBatch.SetVel +.. py:method:: DEMExternObj.SetInitPos .. code-block:: text - SetVel(*args, **kwargs) - Overloaded function. + SetInitPos(self: deme._deme.DEMExternObj, position: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - 1. SetVel(self: deme._deme.DEMClumpBatch, velocities: collections.abc.Sequence[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> None + Set the initial global center-of-mass position before initialization. - Set one global initial linear velocity vector per clump. - 2. SetVel(self: deme._deme.DEMClumpBatch, velocity: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None +.. py:method:: DEMExternObj.SetInitQuat - Set the same global initial linear velocity for every clump. + .. code-block:: text + SetInitQuat(self: deme._deme.DEMExternObj, quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + Set the initial local-to-global orientation quaternion ``(x, y, z, w)`` before initialization. -DEMClumpTemplate ----------------- -.. py:class:: DEMClumpTemplate +.. py:method:: DEMExternObj.SetMass .. code-block:: text - Reusable rigid clump topology composed of one or more sphere components. + SetMass(self: deme._deme.DEMExternObj, mass: typing.SupportsFloat | typing.SupportsIndex) -> None -.. py:method:: DEMClumpTemplate.__init__ + Set the object mass. + + +.. py:method:: DEMExternObj.SetMOI .. code-block:: text - __init__(self: deme._deme.DEMClumpTemplate) -> None + SetMOI(self: deme._deme.DEMExternObj, MOI: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - Create an empty clump template. + Set the three principal moments of inertia in the object-local frame. -.. py:method:: DEMClumpTemplate.AssignName +.. py:attribute:: DEMExternObj.types .. code-block:: text - AssignName(self: deme._deme.DEMClumpTemplate, name: str) -> None + Bound attribute. - Assign the clump type name written to output files. +DEMForceModel +------------- -.. py:method:: DEMClumpTemplate.InformCentroidPrincipal +.. py:class:: DEMForceModel .. code-block:: text - InformCentroidPrincipal(self: deme._deme.DEMClumpTemplate, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], orientation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - - Re-express component positions about the supplied center of mass and principal orientation. - + No class description is currently available. -.. py:method:: DEMClumpTemplate.Mass +.. py:method:: DEMForceModel.__init__ .. code-block:: text - Mass(self: deme._deme.DEMClumpTemplate) -> float + __init__(self: deme._deme.DEMForceModel, model: deme::FORCE_MODEL) -> None - Return the clump mass. + Create a force-model definition of the selected built-in type. -.. py:method:: DEMClumpTemplate.MOI +.. py:method:: DEMForceModel.DefineCustomModel .. code-block:: text - MOI(self: deme._deme.DEMClumpTemplate) -> float3 + DefineCustomModel(self: deme._deme.DEMForceModel, arg0: str) -> None - Return principal moments of inertia in the clump-local principal frame. + Define user-custom force model with a string which is your force calculation code. -.. py:method:: DEMClumpTemplate.Move +.. py:method:: DEMForceModel.DefineCustomModelPrerequisites .. code-block:: text - Move(self: deme._deme.DEMClumpTemplate, translation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + DefineCustomModelPrerequisites(self: deme._deme.DEMForceModel, arg0: str) -> None - Apply a translation and quaternion rotation to the component coordinates. + Define user-custom force model's utility __device__ functions with a string. -.. py:method:: DEMClumpTemplate.ReadComponentFromFile +.. py:method:: DEMForceModel.ReadCustomModelFile .. code-block:: text - ReadComponentFromFile(self: deme._deme.DEMClumpTemplate, filename: str, x_id: str = 'x', y_id: str = 'y', z_id: str = 'z', r_id: str = 'r') -> int + ReadCustomModelFile(self: deme._deme.DEMForceModel, arg0: std::filesystem::__cxx11::path) -> int - Load sphere-component positions and radii from a delimited file. + Read user-custom force model from a file (which by default should reside in kernel/DEMUserScripts), which contains your force calculation code. Returns 0 if read successfully, otherwise 1. -.. py:method:: DEMClumpTemplate.Scale +.. py:method:: DEMForceModel.ReadCustomModelPrerequisitesFile .. code-block:: text - Scale(self: deme._deme.DEMClumpTemplate, factor: typing.SupportsFloat | typing.SupportsIndex) -> None + ReadCustomModelPrerequisitesFile(self: deme._deme.DEMForceModel, arg0: std::filesystem::__cxx11::path) -> int - Uniformly scale component radii and relative positions. + Read user-custom force model's utility __device__ functions from a file (which by default should reside in kernel/DEMUserScripts). Returns 0 if read successfully, otherwise 1. -.. py:method:: DEMClumpTemplate.SetMass +.. py:method:: DEMForceModel.SetForceModelType .. code-block:: text - SetMass(self: deme._deme.DEMClumpTemplate, mass: typing.SupportsFloat | typing.SupportsIndex) -> None + SetForceModelType(self: deme._deme.DEMForceModel, arg0: deme::FORCE_MODEL) -> None - Set the clump mass. + Set the contact force model type -.. py:method:: DEMClumpTemplate.SetMaterial +.. py:method:: DEMForceModel.SetMustHaveMatProp .. code-block:: text - SetMaterial(*args, **kwargs) - Overloaded function. + SetMustHaveMatProp(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None - 1. SetMaterial(self: deme._deme.DEMClumpTemplate, materials: collections.abc.Sequence[deme._deme.DEMMaterial]) -> None + Specifiy the material properties that this force model will use - Assign one material per sphere component. The sequence length must equal the component count. - 2. SetMaterial(self: deme._deme.DEMClumpTemplate, material: deme._deme.DEMMaterial) -> None +.. py:method:: DEMForceModel.SetMustPairwiseMatProp - Assign the same material to every sphere component. + .. code-block:: text + + SetMustPairwiseMatProp(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None + + Specifiy the material properties that are pair-wise (instead of being associated with each individual material). -.. py:method:: DEMClumpTemplate.SetMOI +.. py:method:: DEMForceModel.SetPerContactWildcards .. code-block:: text - SetMOI(self: deme._deme.DEMClumpTemplate, MOI: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + SetPerContactWildcards(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None - Set the three principal moments of inertia in the clump-local frame. + Set the names for the extra quantities that will be associated with each contact pair. For example, history-based models should have 3 float arrays to store contact history. Only float is supported. Note the initial value of all contact wildcard arrays is automatically 0 -.. py:method:: DEMClumpTemplate.SetVolume +.. py:method:: DEMForceModel.SetPerOwnerWildcards .. code-block:: text - SetVolume(self: deme._deme.DEMClumpTemplate, volume: typing.SupportsFloat | typing.SupportsIndex) -> None + SetPerOwnerWildcards(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None - Set the clump volume, which is required for void-ratio queries. + Set the names for the extra quantities that will be associated with each owner. For example, you can use this to associate a cohesion parameter to each particle. Only float is supported. -DEMCombinedInstances --------------------- +DEMInitializer +-------------- -.. py:class:: DEMCombinedInstances +.. py:class:: DEMInitializer .. code-block:: text - A batch of instantiated combined owners and their member-owner metadata. + No class description is currently available. -.. py:method:: DEMCombinedInstances.__init__ +.. py:method:: DEMInitializer.__init__ .. code-block:: text - __init__(self: deme._deme.DEMCombinedInstances) -> None + __init__(self: deme._deme.DEMInitializer) -> None - Create an empty combined-instance batch. -.. py:method:: DEMCombinedInstances.AddOwnerWildcard +DEMInspector +------------ + +.. py:class:: DEMInspector .. code-block:: text - AddOwnerWildcard(*args, **kwargs) - Overloaded function. + Evaluate a configured aggregate or spatial quantity from initialized simulation state. - 1. AddOwnerWildcard(self: deme._deme.DEMCombinedInstances, name: str, vals: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - - Add an owner wildcard to all member owners with per-owner values. +.. py:method:: DEMInspector.__init__ - 2. AddOwnerWildcard(self: deme._deme.DEMCombinedInstances, name: str, val: typing.SupportsFloat | typing.SupportsIndex) -> None + .. code-block:: text - Add an owner wildcard to all member owners with a uniform value. + __init__(self: deme._deme.DEMInspector, arg0: deme::DEMSolver, arg1: deme::DEMDynamicThread, arg2: str) -> None -.. py:method:: DEMCombinedInstances.GetNumOwners +.. py:method:: DEMInspector.GetValue .. code-block:: text - GetNumOwners(self: deme._deme.DEMCombinedInstances) -> int + GetValue(self: deme._deme.DEMInspector) -> float - Get total number of member owners in this combined batch. + Synchronize as needed and return the inspector's current scalar value. -DEMCombinedTemplate -------------------- +DEMMaterial +----------- -.. py:class:: DEMCombinedTemplate +.. py:class:: DEMMaterial .. code-block:: text - Reusable rigid grouping of clump or mesh templates with fixed member-relative transforms. + Material property name/value pairs used by a contact force model. -.. py:method:: DEMCombinedTemplate.__init__ +.. py:method:: DEMMaterial.__init__ .. code-block:: text - __init__(self: deme._deme.DEMCombinedTemplate) -> None - - Create an empty combined-owner template. - + __init__(self: deme._deme.DEMMaterial, properties: collections.abc.Mapping[str, typing.SupportsFloat | typing.SupportsIndex]) -> None + Create material properties. Normally use ``DEMSolver.LoadMaterial`` so the material is registered. -DEMERuntimeDataHelper ---------------------- -.. py:class:: DEMERuntimeDataHelper +.. py:attribute:: DEMMaterial.load_order .. code-block:: text - No class description is currently available. + Bound attribute. -.. py:method:: DEMERuntimeDataHelper.__init__ +.. py:attribute:: DEMMaterial.mat_prop .. code-block:: text - __init__(self: deme._deme.DEMERuntimeDataHelper) -> None + Bound attribute. -.. py:method:: DEMERuntimeDataHelper.SetPathPrefix +DEMMesh +------- + +.. py:class:: DEMMesh .. code-block:: text - Convert a function to be a static method. + Connected triangle-mesh template or external mesh owner. - A static method does not receive an implicit first argument. - To declare a static method, use this idiom: +.. py:method:: DEMMesh.__init__ - class C: - @staticmethod - def f(arg1, arg2, argN): - ... + .. code-block:: text - It can be called either on the class (e.g. C.f()) or on an instance - (e.g. C().f()). Both the class and the instance are ignored, and - neither is passed implicitly as the first argument to the method. + __init__(*args, **kwargs) + Overloaded function. - Static methods in Python are similar to those found in Java or C++. - For a more advanced concept, see the classmethod builtin. + 1. __init__(self: deme._deme.DEMMesh) -> None + Create an empty mesh. + 2. __init__(self: deme._deme.DEMMesh, filename: str) -> None -DEMExternObj ------------- + Load a mesh from a Wavefront OBJ file. -.. py:class:: DEMExternObj + 3. __init__(self: deme._deme.DEMMesh, filename: str, material: deme._deme.DEMMaterial) -> None - .. code-block:: text + Load a Wavefront OBJ mesh and assign one material to every triangle. - Analytical external owner composed of planes, cylinders, and related primitives. -.. py:method:: DEMExternObj.__init__ +.. py:method:: DEMMesh.ArePatchesExplicitlySet .. code-block:: text - __init__(self: deme._deme.DEMExternObj) -> None + ArePatchesExplicitlySet(self: deme._deme.DEMMesh) -> bool - Create an empty analytical external object. + Return whether patch IDs were explicitly supplied or computed. -.. py:method:: DEMExternObj.AddCylinder +.. py:method:: DEMMesh.ArePatchLocationsExplicitlySet .. code-block:: text - AddCylinder(self: deme._deme.DEMExternObj, pos: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], axis: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], rad: typing.SupportsFloat | typing.SupportsIndex, material: deme._deme.DEMMaterial, normal: bool = False) -> None - - Add an infinite cylinder along a user-specified object-local axis. + ArePatchLocationsExplicitlySet(self: deme._deme.DEMMesh) -> bool -.. py:method:: DEMExternObj.AddPlane +.. py:method:: DEMMesh.Clear .. code-block:: text - AddPlane(self: deme._deme.DEMExternObj, point: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], normal: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], material: deme._deme.DEMMaterial) -> None + Clear(self: deme._deme.DEMMesh) -> None - Add an infinite plane specified by a point and normal in the object's local frame. + Remove all mesh geometry and cached attributes. -.. py:method:: DEMExternObj.AddZCylinder +.. py:method:: DEMMesh.ClearWildcards .. code-block:: text - AddZCylinder(self: deme._deme.DEMExternObj, pos: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], rad: typing.SupportsFloat | typing.SupportsIndex, material: deme._deme.DEMMaterial, normal: bool = False) -> None + ClearWildcards(self: deme._deme.DEMMesh) -> None - Add an infinite cylinder aligned with the object-local Z axis. + Remove cached triangle wildcard values. -.. py:attribute:: DEMExternObj.entity_params +.. py:method:: DEMMesh.DisableShell .. code-block:: text - Bound attribute. - -.. py:attribute:: DEMExternObj.family_code + DisableShell(self: deme._deme.DEMMesh) -> None - .. code-block:: text + Disable finite-thickness shell mode. - Bound attribute. -.. py:attribute:: DEMExternObj.init_oriQ +.. py:method:: DEMMesh.GetCoordsVertices .. code-block:: text - Bound attribute. - -.. py:attribute:: DEMExternObj.init_pos + GetCoordsVertices(self: deme._deme.DEMMesh) -> list[list[float]] - .. code-block:: text + Return mesh vertex coordinates as ``[[x, y, z], ...]``. - Bound attribute. -.. py:attribute:: DEMExternObj.load_order +.. py:method:: DEMMesh.GetIndicesVertexes .. code-block:: text - Bound attribute. - -.. py:method:: DEMExternObj.Mass + GetIndicesVertexes(self: deme._deme.DEMMesh) -> list[list[int]] - .. code-block:: text + Return triangle vertex-index triplets. - Mass(self: deme._deme.DEMExternObj) -> float - Return the object's mass. +.. py:method:: DEMMesh.GetNumNodes + .. code-block:: text -.. py:attribute:: DEMExternObj.mass + GetNumNodes(self: deme._deme.DEMMesh) -> int - .. code-block:: text + Get the number of nodes in the mesh - Bound attribute. -.. py:attribute:: DEMExternObj.materials +.. py:method:: DEMMesh.GetNumPatches .. code-block:: text - Bound attribute. - -.. py:attribute:: DEMExternObj.MOI + GetNumPatches(self: deme._deme.DEMMesh) -> int - .. code-block:: text + Get the number of mesh patches. - Bound attribute. -.. py:method:: DEMExternObj.SetFamily +.. py:method:: DEMMesh.GetNumTriangles .. code-block:: text - SetFamily(self: deme._deme.DEMExternObj, family: typing.SupportsInt | typing.SupportsIndex) -> None + GetNumTriangles(self: deme._deme.DEMMesh) -> int - Set the contact family number before initialization. + Get the number of triangles already added to this mesh -.. py:method:: DEMExternObj.SetInitPos +.. py:method:: DEMMesh.GetPatchIDs .. code-block:: text - SetInitPos(self: deme._deme.DEMExternObj, position: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + GetPatchIDs(self: deme._deme.DEMMesh) -> list[int] - Set the initial global center-of-mass position before initialization. + Get one patch ID per triangle. -.. py:method:: DEMExternObj.SetInitQuat +.. py:method:: DEMMesh.GetPatchLocations .. code-block:: text - SetInitQuat(self: deme._deme.DEMExternObj, quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + GetPatchLocations(self: deme._deme.DEMMesh) -> list[float3] - Set the initial local-to-global orientation quaternion ``(x, y, z, w)`` before initialization. + Read cached patch centers; call tracker GetMesh first after device updates to synchronize. -.. py:method:: DEMExternObj.SetMass +.. py:method:: DEMMesh.GetShellThickness .. code-block:: text - SetMass(self: deme._deme.DEMExternObj, mass: typing.SupportsFloat | typing.SupportsIndex) -> None + GetShellThickness(self: deme._deme.DEMMesh) -> float - Set the object mass. + Return the full shell thickness. -.. py:method:: DEMExternObj.SetMOI +.. py:method:: DEMMesh.GetTriangle .. code-block:: text - SetMOI(self: deme._deme.DEMExternObj, MOI: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + GetTriangle(self: deme._deme.DEMMesh, arg0: typing.SupportsInt | typing.SupportsIndex) -> deme::DEMTriangle - Set the three principal moments of inertia in the object-local frame. + Access the n-th triangle in mesh -.. py:attribute:: DEMExternObj.types +.. py:method:: DEMMesh.InformCentroidPrincipal .. code-block:: text - Bound attribute. + InformCentroidPrincipal(self: deme._deme.DEMMesh, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], orientation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + Re-express mesh vertices about the supplied center of mass and principal orientation. -DEMForceModel -------------- -.. py:class:: DEMForceModel +.. py:method:: DEMMesh.IsShell .. code-block:: text - No class description is currently available. + IsShell(self: deme._deme.DEMMesh) -> bool + + Return whether this mesh is configured as a shell. -.. py:method:: DEMForceModel.__init__ + +.. py:method:: DEMMesh.LoadWavefrontMesh .. code-block:: text - __init__(self: deme._deme.DEMForceModel, model: deme::FORCE_MODEL) -> None + LoadWavefrontMesh(self: deme._deme.DEMMesh, input_file: str, load_normals: bool = True, load_uv: bool = False) -> bool - Create a force-model definition of the selected built-in type. + Load a triangle mesh saved as a Wavefront .obj file -.. py:method:: DEMForceModel.DefineCustomModel +.. py:method:: DEMMesh.Mass .. code-block:: text - DefineCustomModel(self: deme._deme.DEMForceModel, arg0: str) -> None + Mass(self: deme._deme.DEMMesh) -> float - Define user-custom force model with a string which is your force calculation code. + Return the mesh owner's mass. -.. py:method:: DEMForceModel.DefineCustomModelPrerequisites +.. py:method:: DEMMesh.Mirror .. code-block:: text - DefineCustomModelPrerequisites(self: deme._deme.DEMForceModel, arg0: str) -> None + Mirror(self: deme._deme.DEMMesh, point: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], normal: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - Define user-custom force model's utility __device__ functions with a string. + Mirror vertices about a plane specified by a point and normal. -.. py:method:: DEMForceModel.ReadCustomModelFile +.. py:method:: DEMMesh.MOI .. code-block:: text - ReadCustomModelFile(self: deme._deme.DEMForceModel, arg0: std::filesystem::__cxx11::path) -> int + MOI(self: deme._deme.DEMMesh) -> float3 - Read user-custom force model from a file (which by default should reside in kernel/DEMUserScripts), which contains your force calculation code. Returns 0 if read successfully, otherwise 1. + Return principal moments of inertia in the mesh-local frame. -.. py:method:: DEMForceModel.ReadCustomModelPrerequisitesFile +.. py:method:: DEMMesh.Move .. code-block:: text - ReadCustomModelPrerequisitesFile(self: deme._deme.DEMForceModel, arg0: std::filesystem::__cxx11::path) -> int + Move(self: deme._deme.DEMMesh, translation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - Read user-custom force model's utility __device__ functions from a file (which by default should reside in kernel/DEMUserScripts). Returns 0 if read successfully, otherwise 1. + Apply a translation and quaternion rotation to mesh vertices. -.. py:method:: DEMForceModel.SetForceModelType +.. py:method:: DEMMesh.Scale .. code-block:: text - SetForceModelType(self: deme._deme.DEMForceModel, arg0: deme::FORCE_MODEL) -> None - - Set the contact force model type - + Scale(*args, **kwargs) + Overloaded function. -.. py:method:: DEMForceModel.SetMustHaveMatProp + 1. Scale(self: deme._deme.DEMMesh, factor: typing.SupportsFloat | typing.SupportsIndex) -> None - .. code-block:: text + Uniformly scale mesh vertices. - SetMustHaveMatProp(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None + 2. Scale(self: deme._deme.DEMMesh, factors: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - Specifiy the material properties that this force model will use + Scale mesh vertices independently along X, Y, and Z. -.. py:method:: DEMForceModel.SetMustPairwiseMatProp +.. py:method:: DEMMesh.SetEachTriangleAsPatch .. code-block:: text - SetMustPairwiseMatProp(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None + SetEachTriangleAsPatch(self: deme._deme.DEMMesh) -> None - Specifiy the material properties that are pair-wise (instead of being associated with each individual material). + Assign every triangle to its own patch using consecutive patch IDs. -.. py:method:: DEMForceModel.SetPerContactWildcards +.. py:method:: DEMMesh.SetFamily .. code-block:: text - SetPerContactWildcards(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None + SetFamily(self: deme._deme.DEMMesh, family: typing.SupportsInt | typing.SupportsIndex) -> None - Set the names for the extra quantities that will be associated with each contact pair. For example, history-based models should have 3 float arrays to store contact history. Only float is supported. Note the initial value of all contact wildcard arrays is automatically 0 + Set the contact family number before initialization. -.. py:method:: DEMForceModel.SetPerOwnerWildcards +.. py:method:: DEMMesh.SetInitPos .. code-block:: text - SetPerOwnerWildcards(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None + SetInitPos(self: deme._deme.DEMMesh, position: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - Set the names for the extra quantities that will be associated with each owner. For example, you can use this to associate a cohesion parameter to each particle. Only float is supported. + Set the initial global center-of-mass position. +.. py:method:: DEMMesh.SetInitQuat -DEMInitializer --------------- + .. code-block:: text -.. py:class:: DEMInitializer + SetInitQuat(self: deme._deme.DEMMesh, quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - .. code-block:: text + Set the initial local-to-global orientation quaternion ``(x, y, z, w)``. - No class description is currently available. -.. py:method:: DEMInitializer.__init__ +.. py:method:: DEMMesh.SetMass .. code-block:: text - __init__(self: deme._deme.DEMInitializer) -> None + SetMass(self: deme._deme.DEMMesh, mass: typing.SupportsFloat | typing.SupportsIndex) -> None + Set the mesh owner's mass. -DEMInspector ------------- - -.. py:class:: DEMInspector +.. py:method:: DEMMesh.SetMaterial .. code-block:: text - Evaluate a configured aggregate or spatial quantity from initialized simulation state. + SetMaterial(*args, **kwargs) + Overloaded function. -.. py:method:: DEMInspector.__init__ + 1. SetMaterial(self: deme._deme.DEMMesh, material: deme._deme.DEMMaterial) -> None - .. code-block:: text + Assign the same material to every mesh triangle. - __init__(self: deme._deme.DEMInspector, arg0: deme::DEMSolver, arg1: deme::DEMDynamicThread, arg2: str) -> None + 2. SetMaterial(self: deme._deme.DEMMesh, materials: collections.abc.Sequence[deme._deme.DEMMaterial]) -> None + + Assign one material per triangle. The sequence length must equal the triangle count. -.. py:method:: DEMInspector.GetValue +.. py:method:: DEMMesh.SetMOI .. code-block:: text - GetValue(self: deme._deme.DEMInspector) -> float + SetMOI(self: deme._deme.DEMMesh, MOI: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None - Synchronize as needed and return the inspector's current scalar value. + Set the three principal moments of inertia in the mesh-local frame. +.. py:method:: DEMMesh.SetPatchIDs -DEMMaterial ------------ + .. code-block:: text -.. py:class:: DEMMaterial + SetPatchIDs(self: deme._deme.DEMMesh, patch_ids: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> None - .. code-block:: text + Set one patch ID per triangle. - Material property name/value pairs used by a contact force model. -.. py:method:: DEMMaterial.__init__ +.. py:method:: DEMMesh.SetPatchLocations .. code-block:: text - __init__(self: deme._deme.DEMMaterial, properties: collections.abc.Mapping[str, typing.SupportsFloat | typing.SupportsIndex]) -> None + SetPatchLocations(self: deme._deme.DEMMesh, centers: collections.abc.Sequence[float3]) -> None - Create material properties. Normally use ``DEMSolver.LoadMaterial`` so the material is registered. + Set local patch centers before initialization. Use tracker UpdateMeshPatchLocations at runtime. -.. py:attribute:: DEMMaterial.load_order +.. py:method:: DEMMesh.SetShellThickness .. code-block:: text - Bound attribute. - -.. py:attribute:: DEMMaterial.mat_prop + SetShellThickness(self: deme._deme.DEMMesh, thickness: typing.SupportsFloat | typing.SupportsIndex) -> None - .. code-block:: text + Treat this mesh as a finite-thickness shell. - Bound attribute. +.. py:method:: DEMMesh.SplitIntoConvexPatches -DEMMesh -------- + .. code-block:: text -.. py:class:: DEMMesh + SplitIntoConvexPatches(self: deme._deme.DEMMesh, hard_angle_deg: typing.SupportsFloat | typing.SupportsIndex = 30.0) -> int - .. code-block:: text + Split the mesh into connected angle-threshold patches. - Connected triangle-mesh template or external mesh owner. -.. py:method:: DEMMesh.__init__ +.. py:method:: DEMMesh.UseNormals .. code-block:: text - __init__(*args, **kwargs) - Overloaded function. + UseNormals(self: deme._deme.DEMMesh, use: bool = True) -> None - 1. __init__(self: deme._deme.DEMMesh) -> None + Instruct that when the mesh is initialized into the system, it will re-order the nodes of each triangle so that the normals derived from right-hand-rule are the same as the normals in the mesh file - Create an empty mesh. - 2. __init__(self: deme._deme.DEMMesh, filename: str) -> None +.. py:method:: DEMMesh.WriteWavefront - Load a mesh from a Wavefront OBJ file. + .. code-block:: text - 3. __init__(self: deme._deme.DEMMesh, filename: str, material: deme._deme.DEMMaterial) -> None + WriteWavefront(self: str, output_file: collections.abc.Sequence[deme._deme.DEMMesh]) -> None - Load a Wavefront OBJ mesh and assign one material to every triangle. + Write the mesh geometry to a Wavefront OBJ file. -.. py:method:: DEMMesh.ArePatchesExplicitlySet - .. code-block:: text +DEMTrackedObj +------------- - ArePatchesExplicitlySet(self: deme._deme.DEMMesh) -> bool +.. py:class:: DEMTrackedObj - Return whether patch IDs were explicitly supplied or computed. + .. code-block:: text + No class description is currently available. -.. py:method:: DEMMesh.Clear +.. py:method:: DEMTrackedObj.__init__ .. code-block:: text - Clear(self: deme._deme.DEMMesh) -> None + __init__(self: deme._deme.DEMTrackedObj, arg0: deme._deme.DEMTrackedObj) -> None - Remove all mesh geometry and cached attributes. -.. py:method:: DEMMesh.ClearWildcards +FORCE_MODEL +----------- + +.. py:class:: FORCE_MODEL .. code-block:: text - ClearWildcards(self: deme._deme.DEMMesh) -> None + Members: - Remove cached triangle wildcard values. + HERTZIAN + HERTZIAN_FRICTIONLES -.. py:method:: DEMMesh.DisableShell + CUSTOM - .. code-block:: text +.. py:method:: FORCE_MODEL.__init__ - DisableShell(self: deme._deme.DEMMesh) -> None + .. code-block:: text - Disable finite-thickness shell mode. + __init__(self: deme._deme.FORCE_MODEL, value: typing.SupportsInt | typing.SupportsIndex) -> None -.. py:method:: DEMMesh.GetCoordsVertices +.. py:attribute:: FORCE_MODEL.CUSTOM .. code-block:: text - GetCoordsVertices(self: deme._deme.DEMMesh) -> list[list[float]] + Members: - Return mesh vertex coordinates as ``[[x, y, z], ...]``. + HERTZIAN + HERTZIAN_FRICTIONLES -.. py:method:: DEMMesh.GetIndicesVertexes + CUSTOM + +.. py:attribute:: FORCE_MODEL.HERTZIAN .. code-block:: text - GetIndicesVertexes(self: deme._deme.DEMMesh) -> list[list[int]] + Members: - Return triangle vertex-index triplets. + HERTZIAN + HERTZIAN_FRICTIONLES -.. py:method:: DEMMesh.GetNumNodes + CUSTOM + +.. py:attribute:: FORCE_MODEL.HERTZIAN_FRICTIONLES .. code-block:: text - GetNumNodes(self: deme._deme.DEMMesh) -> int + Members: - Get the number of nodes in the mesh + HERTZIAN + HERTZIAN_FRICTIONLES -.. py:method:: DEMMesh.GetNumPatches + CUSTOM - .. code-block:: text - GetNumPatches(self: deme._deme.DEMMesh) -> int +GridSampler +----------- - Get the number of mesh patches. +.. py:class:: GridSampler + .. code-block:: text -.. py:method:: DEMMesh.GetNumTriangles + Regular-grid volumetric point sampler. + +.. py:method:: GridSampler.__init__ .. code-block:: text - GetNumTriangles(self: deme._deme.DEMMesh) -> int + __init__(self: deme._deme.GridSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None - Get the number of triangles already added to this mesh + Create a regular-grid sampler with uniform point spacing. -.. py:method:: DEMMesh.GetPatchIDs +.. py:method:: GridSampler.GetSeparation .. code-block:: text - GetPatchIDs(self: deme._deme.DEMMesh) -> list[int] + GetSeparation(self: deme._deme.GridSampler) -> float - Get one patch ID per triangle. + Return the current grid spacing. -.. py:method:: DEMMesh.GetShellThickness +.. py:method:: GridSampler.SampleBox .. code-block:: text - GetShellThickness(self: deme._deme.DEMMesh) -> float + SampleBox(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], halfDim: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[list[float]] - Return the full shell thickness. + Return points sampled from the specified box volume. -.. py:method:: DEMMesh.GetTriangle +.. py:method:: GridSampler.SampleCylinderX .. code-block:: text - GetTriangle(self: deme._deme.DEMMesh, arg0: typing.SupportsInt | typing.SupportsIndex) -> deme::DEMTriangle + SampleCylinderX(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] - Access the n-th triangle in mesh + Return points sampled from an X-aligned cylindrical volume. -.. py:method:: DEMMesh.InformCentroidPrincipal +.. py:method:: GridSampler.SampleCylinderY .. code-block:: text - InformCentroidPrincipal(self: deme._deme.DEMMesh, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], orientation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + SampleCylinderY(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] - Re-express mesh vertices about the supplied center of mass and principal orientation. + Return points sampled from a Y-aligned cylindrical volume. -.. py:method:: DEMMesh.IsShell +.. py:method:: GridSampler.SampleCylinderZ .. code-block:: text - IsShell(self: deme._deme.DEMMesh) -> bool + SampleCylinderZ(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] - Return whether this mesh is configured as a shell. + Return points sampled from a Z-aligned cylindrical volume. -.. py:method:: DEMMesh.LoadWavefrontMesh +.. py:method:: GridSampler.SampleSphere .. code-block:: text - LoadWavefrontMesh(self: deme._deme.DEMMesh, input_file: str, load_normals: bool = True, load_uv: bool = False) -> bool + SampleSphere(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] - Load a triangle mesh saved as a Wavefront .obj file + Return points sampled from the specified spherical volume. -.. py:method:: DEMMesh.Mass +.. py:method:: GridSampler.SetSeparation .. code-block:: text - Mass(self: deme._deme.DEMMesh) -> float + SetSeparation(self: deme._deme.GridSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None - Return the mesh owner's mass. + Set the point spacing used by subsequent samples. -.. py:method:: DEMMesh.Mirror - .. code-block:: text +HCPSampler +---------- - Mirror(self: deme._deme.DEMMesh, point: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], normal: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None +.. py:class:: HCPSampler - Mirror vertices about a plane specified by a point and normal. + .. code-block:: text + Hexagonally close-packed volumetric point sampler. -.. py:method:: DEMMesh.MOI +.. py:method:: HCPSampler.__init__ .. code-block:: text - MOI(self: deme._deme.DEMMesh) -> float3 + __init__(self: deme._deme.HCPSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None - Return principal moments of inertia in the mesh-local frame. + Create an HCP sampler with the requested point separation. -.. py:method:: DEMMesh.Move +.. py:method:: HCPSampler.GetSeparation .. code-block:: text - Move(self: deme._deme.DEMMesh, translation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + GetSeparation(self: deme._deme.HCPSampler) -> float - Apply a translation and quaternion rotation to mesh vertices. + Return the current point separation. -.. py:method:: DEMMesh.Scale +.. py:method:: HCPSampler.SampleBox .. code-block:: text - Scale(*args, **kwargs) - Overloaded function. - - 1. Scale(self: deme._deme.DEMMesh, factor: typing.SupportsFloat | typing.SupportsIndex) -> None - - Uniformly scale mesh vertices. - - 2. Scale(self: deme._deme.DEMMesh, factors: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + SampleBox(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], halfDim: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[list[float]] - Scale mesh vertices independently along X, Y, and Z. + Return points sampled from the specified box volume. -.. py:method:: DEMMesh.SetFamily +.. py:method:: HCPSampler.SampleCylinderX .. code-block:: text - SetFamily(self: deme._deme.DEMMesh, family: typing.SupportsInt | typing.SupportsIndex) -> None + SampleCylinderX(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] - Set the contact family number before initialization. + Return points sampled from an X-aligned cylindrical volume. -.. py:method:: DEMMesh.SetInitPos +.. py:method:: HCPSampler.SampleCylinderY .. code-block:: text - SetInitPos(self: deme._deme.DEMMesh, position: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + SampleCylinderY(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] - Set the initial global center-of-mass position. + Return points sampled from a Y-aligned cylindrical volume. -.. py:method:: DEMMesh.SetInitQuat +.. py:method:: HCPSampler.SampleCylinderZ .. code-block:: text - SetInitQuat(self: deme._deme.DEMMesh, quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + SampleCylinderZ(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] - Set the initial local-to-global orientation quaternion ``(x, y, z, w)``. + Return points sampled from a Z-aligned cylindrical volume. -.. py:method:: DEMMesh.SetMass +.. py:method:: HCPSampler.SampleSphere .. code-block:: text - SetMass(self: deme._deme.DEMMesh, mass: typing.SupportsFloat | typing.SupportsIndex) -> None + SampleSphere(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] - Set the mesh owner's mass. + Return points sampled from the specified spherical volume. -.. py:method:: DEMMesh.SetMaterial +.. py:method:: HCPSampler.SetSeparation .. code-block:: text - SetMaterial(*args, **kwargs) - Overloaded function. - - 1. SetMaterial(self: deme._deme.DEMMesh, material: deme._deme.DEMMaterial) -> None + SetSeparation(self: deme._deme.HCPSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None - Assign the same material to every mesh triangle. + Set the point separation used by subsequent samples. - 2. SetMaterial(self: deme._deme.DEMMesh, materials: collections.abc.Sequence[deme._deme.DEMMaterial]) -> None - Assign one material per triangle. The sequence length must equal the triangle count. +MESH_FORMAT +----------- -.. py:method:: DEMMesh.SetMOI +.. py:class:: MESH_FORMAT .. code-block:: text - SetMOI(self: deme._deme.DEMMesh, MOI: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + Members: - Set the three principal moments of inertia in the mesh-local frame. + VTK + OBJ -.. py:method:: DEMMesh.SetPatchIDs + STL - .. code-block:: text + PLY - SetPatchIDs(self: deme._deme.DEMMesh, patch_ids: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> None +.. py:method:: MESH_FORMAT.__init__ - Set one patch ID per triangle. + .. code-block:: text + __init__(self: deme._deme.MESH_FORMAT, value: typing.SupportsInt | typing.SupportsIndex) -> None -.. py:method:: DEMMesh.SetShellThickness - .. code-block:: text +.. py:attribute:: MESH_FORMAT.OBJ - SetShellThickness(self: deme._deme.DEMMesh, thickness: typing.SupportsFloat | typing.SupportsIndex) -> None + .. code-block:: text - Treat this mesh as a finite-thickness shell. + Members: + VTK -.. py:method:: DEMMesh.SplitIntoConvexPatches + OBJ - .. code-block:: text + STL - SplitIntoConvexPatches(self: deme._deme.DEMMesh, hard_angle_deg: typing.SupportsFloat | typing.SupportsIndex = 30.0) -> int + PLY - Split the mesh into connected angle-threshold patches. +.. py:attribute:: MESH_FORMAT.PLY + .. code-block:: text -.. py:method:: DEMMesh.UseNormals + Members: - .. code-block:: text + VTK - UseNormals(self: deme._deme.DEMMesh, use: bool = True) -> None + OBJ - Instruct that when the mesh is initialized into the system, it will re-order the nodes of each triangle so that the normals derived from right-hand-rule are the same as the normals in the mesh file + STL + PLY -.. py:method:: DEMMesh.WriteWavefront +.. py:attribute:: MESH_FORMAT.STL .. code-block:: text - WriteWavefront(self: str, output_file: collections.abc.Sequence[deme._deme.DEMMesh]) -> None + Members: - Write the mesh geometry to a Wavefront OBJ file. + VTK + OBJ + STL -DEMTrackedObj -------------- + PLY -.. py:class:: DEMTrackedObj +.. py:attribute:: MESH_FORMAT.VTK .. code-block:: text - No class description is currently available. + Members: -.. py:method:: DEMTrackedObj.__init__ + VTK - .. code-block:: text + OBJ - __init__(self: deme._deme.DEMTrackedObj, arg0: deme._deme.DEMTrackedObj) -> None + STL + PLY -FORCE_MODEL ------------ +MESH_OUTPUT_CONTENT +------------------- -.. py:class:: FORCE_MODEL +.. py:class:: MESH_OUTPUT_CONTENT .. code-block:: text Members: - HERTZIAN + XYZ - HERTZIAN_FRICTIONLES + QUAT - CUSTOM + ABSV -.. py:method:: FORCE_MODEL.__init__ + VEL - .. code-block:: text + ANG_VEL - __init__(self: deme._deme.FORCE_MODEL, value: typing.SupportsInt | typing.SupportsIndex) -> None + ABS_ACC + ACC -.. py:attribute:: FORCE_MODEL.CUSTOM + ANG_ACC - .. code-block:: text + FAMILY - Members: + MAT - HERTZIAN + OWNER_WILDCARD - HERTZIAN_FRICTIONLES + GEO_WILDCARD - CUSTOM + OWNER -.. py:attribute:: FORCE_MODEL.HERTZIAN + MESH_ID - .. code-block:: text + TRI_ID - Members: + PATCH_ID - HERTZIAN +.. py:method:: MESH_OUTPUT_CONTENT.__init__ - HERTZIAN_FRICTIONLES + .. code-block:: text - CUSTOM + __init__(self: deme._deme.MESH_OUTPUT_CONTENT, value: typing.SupportsInt | typing.SupportsIndex) -> None -.. py:attribute:: FORCE_MODEL.HERTZIAN_FRICTIONLES + +.. py:attribute:: MESH_OUTPUT_CONTENT.ABS_ACC .. code-block:: text Members: - HERTZIAN - - HERTZIAN_FRICTIONLES - - CUSTOM + XYZ + QUAT -GridSampler ------------ + ABSV -.. py:class:: GridSampler + VEL - .. code-block:: text + ANG_VEL - Regular-grid volumetric point sampler. + ABS_ACC -.. py:method:: GridSampler.__init__ + ACC - .. code-block:: text + ANG_ACC - __init__(self: deme._deme.GridSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + FAMILY - Create a regular-grid sampler with uniform point spacing. + MAT + OWNER_WILDCARD -.. py:method:: GridSampler.GetSeparation + GEO_WILDCARD - .. code-block:: text + OWNER - GetSeparation(self: deme._deme.GridSampler) -> float + MESH_ID - Return the current grid spacing. + TRI_ID + PATCH_ID -.. py:method:: GridSampler.SampleBox +.. py:attribute:: MESH_OUTPUT_CONTENT.ABSV .. code-block:: text - SampleBox(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], halfDim: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[list[float]] - - Return points sampled from the specified box volume. + Members: + XYZ -.. py:method:: GridSampler.SampleCylinderX + QUAT - .. code-block:: text + ABSV - SampleCylinderX(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + VEL - Return points sampled from an X-aligned cylindrical volume. + ANG_VEL + ABS_ACC -.. py:method:: GridSampler.SampleCylinderY + ACC - .. code-block:: text + ANG_ACC - SampleCylinderY(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + FAMILY - Return points sampled from a Y-aligned cylindrical volume. + MAT + OWNER_WILDCARD -.. py:method:: GridSampler.SampleCylinderZ + GEO_WILDCARD - .. code-block:: text + OWNER - SampleCylinderZ(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + MESH_ID - Return points sampled from a Z-aligned cylindrical volume. + TRI_ID + PATCH_ID -.. py:method:: GridSampler.SampleSphere +.. py:attribute:: MESH_OUTPUT_CONTENT.ACC .. code-block:: text - SampleSphere(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] - - Return points sampled from the specified spherical volume. + Members: + XYZ -.. py:method:: GridSampler.SetSeparation + QUAT - .. code-block:: text + ABSV - SetSeparation(self: deme._deme.GridSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + VEL - Set the point spacing used by subsequent samples. + ANG_VEL + ABS_ACC + ACC -HCPSampler ----------- + ANG_ACC -.. py:class:: HCPSampler + FAMILY - .. code-block:: text + MAT - Hexagonally close-packed volumetric point sampler. + OWNER_WILDCARD -.. py:method:: HCPSampler.__init__ + GEO_WILDCARD - .. code-block:: text + OWNER - __init__(self: deme._deme.HCPSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + MESH_ID - Create an HCP sampler with the requested point separation. + TRI_ID + PATCH_ID -.. py:method:: HCPSampler.GetSeparation +.. py:attribute:: MESH_OUTPUT_CONTENT.ANG_ACC .. code-block:: text - GetSeparation(self: deme._deme.HCPSampler) -> float - - Return the current point separation. + Members: + XYZ -.. py:method:: HCPSampler.SampleBox + QUAT - .. code-block:: text + ABSV - SampleBox(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], halfDim: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[list[float]] + VEL - Return points sampled from the specified box volume. + ANG_VEL + ABS_ACC -.. py:method:: HCPSampler.SampleCylinderX + ACC - .. code-block:: text + ANG_ACC - SampleCylinderX(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + FAMILY - Return points sampled from an X-aligned cylindrical volume. + MAT + OWNER_WILDCARD -.. py:method:: HCPSampler.SampleCylinderY + GEO_WILDCARD - .. code-block:: text + OWNER - SampleCylinderY(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + MESH_ID - Return points sampled from a Y-aligned cylindrical volume. + TRI_ID + PATCH_ID -.. py:method:: HCPSampler.SampleCylinderZ +.. py:attribute:: MESH_OUTPUT_CONTENT.ANG_VEL .. code-block:: text - SampleCylinderZ(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + Members: - Return points sampled from a Z-aligned cylindrical volume. + XYZ + QUAT -.. py:method:: HCPSampler.SampleSphere + ABSV - .. code-block:: text + VEL - SampleSphere(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + ANG_VEL - Return points sampled from the specified spherical volume. + ABS_ACC + ACC -.. py:method:: HCPSampler.SetSeparation + ANG_ACC - .. code-block:: text + FAMILY - SetSeparation(self: deme._deme.HCPSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + MAT - Set the point separation used by subsequent samples. + OWNER_WILDCARD + GEO_WILDCARD + OWNER -MESH_FORMAT ------------ + MESH_ID -.. py:class:: MESH_FORMAT + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.FAMILY .. code-block:: text Members: - VTK + XYZ - OBJ + QUAT - STL + ABSV - PLY + VEL -.. py:method:: MESH_FORMAT.__init__ + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.GEO_WILDCARD .. code-block:: text - __init__(self: deme._deme.MESH_FORMAT, value: typing.SupportsInt | typing.SupportsIndex) -> None + Members: + XYZ -.. py:attribute:: MESH_FORMAT.OBJ + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.MAT .. code-block:: text Members: - VTK + XYZ - OBJ + QUAT - STL + ABSV - PLY + VEL -.. py:attribute:: MESH_FORMAT.PLY + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.MESH_ID .. code-block:: text Members: - VTK + XYZ - OBJ + QUAT - STL + ABSV - PLY + VEL -.. py:attribute:: MESH_FORMAT.STL + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.OWNER .. code-block:: text Members: - VTK + XYZ - OBJ + QUAT - STL + ABSV - PLY + VEL -.. py:attribute:: MESH_FORMAT.VTK + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.OWNER_WILDCARD .. code-block:: text Members: - VTK + XYZ - OBJ + QUAT - STL + ABSV - PLY + VEL + ANG_VEL -OUTPUT_CONTENT --------------- + ABS_ACC -.. py:class:: OUTPUT_CONTENT + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.PATCH_ID .. code-block:: text @@ -4766,14 +5225,55 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:method:: OUTPUT_CONTENT.__init__ + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.QUAT .. code-block:: text - __init__(self: deme._deme.OUTPUT_CONTENT, value: typing.SupportsInt | typing.SupportsIndex) -> None + Members: + XYZ -.. py:attribute:: OUTPUT_CONTENT.ABS_ACC + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.TRI_ID .. code-block:: text @@ -4801,7 +5301,17 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.ABSV + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.VEL .. code-block:: text @@ -4829,7 +5339,17 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.ACC + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + +.. py:attribute:: MESH_OUTPUT_CONTENT.XYZ .. code-block:: text @@ -4857,7 +5377,21 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.ANG_ACC + GEO_WILDCARD + + OWNER + + MESH_ID + + TRI_ID + + PATCH_ID + + +OUTPUT_CONTENT +-------------- + +.. py:class:: OUTPUT_CONTENT .. code-block:: text @@ -4885,7 +5419,14 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.ANG_VEL +.. py:method:: OUTPUT_CONTENT.__init__ + + .. code-block:: text + + __init__(self: deme._deme.OUTPUT_CONTENT, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: OUTPUT_CONTENT.ABS_ACC .. code-block:: text @@ -4913,7 +5454,7 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.FAMILY +.. py:attribute:: OUTPUT_CONTENT.ABSV .. code-block:: text @@ -4941,7 +5482,7 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.MAT +.. py:attribute:: OUTPUT_CONTENT.ACC .. code-block:: text @@ -4969,7 +5510,7 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.OWNER_WILDCARD +.. py:attribute:: OUTPUT_CONTENT.ANG_ACC .. code-block:: text @@ -4997,7 +5538,7 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.QUAT +.. py:attribute:: OUTPUT_CONTENT.ANG_VEL .. code-block:: text @@ -5025,7 +5566,7 @@ OUTPUT_CONTENT OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.VEL +.. py:attribute:: OUTPUT_CONTENT.FAMILY .. code-block:: text @@ -5037,399 +5578,1584 @@ OUTPUT_CONTENT ABSV - VEL + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.MAT + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.OWNER_WILDCARD + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.QUAT + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.VEL + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.XYZ + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + + +OUTPUT_FORMAT +------------- + +.. py:class:: OUTPUT_FORMAT + + .. code-block:: text + + Members: + + CSV + + BINARY + + VTK + +.. py:method:: OUTPUT_FORMAT.__init__ + + .. code-block:: text + + __init__(self: deme._deme.OUTPUT_FORMAT, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: OUTPUT_FORMAT.BINARY + + .. code-block:: text + + Members: + + CSV + + BINARY + + VTK + +.. py:attribute:: OUTPUT_FORMAT.CSV + + .. code-block:: text + + Members: + + CSV + + BINARY + + VTK + +.. py:attribute:: OUTPUT_FORMAT.VTK + + .. code-block:: text + + Members: + + CSV + + BINARY + + VTK + + +OWNER_TYPE +---------- + +.. py:class:: OWNER_TYPE + + .. code-block:: text + + Members: + + CLUMP + + ANALYTICAL + + MESH + +.. py:method:: OWNER_TYPE.__init__ + + .. code-block:: text + + __init__(self: deme._deme.OWNER_TYPE, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: OWNER_TYPE.ANALYTICAL + + .. code-block:: text + + Members: + + CLUMP + + ANALYTICAL + + MESH + +.. py:attribute:: OWNER_TYPE.CLUMP + + .. code-block:: text + + Members: + + CLUMP + + ANALYTICAL + + MESH + +.. py:attribute:: OWNER_TYPE.MESH + + .. code-block:: text + + Members: + + CLUMP + + ANALYTICAL + + MESH + + +PDSampler +--------- + +.. py:class:: PDSampler + + .. code-block:: text + + Poisson-disk volumetric sampler enforcing a minimum point separation. + +.. py:method:: PDSampler.__init__ + + .. code-block:: text + + __init__(self: deme._deme.PDSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + + Create a sampler with the requested minimum separation. + + +.. py:method:: PDSampler.GetSeparation + + .. code-block:: text + + GetSeparation(self: deme._deme.PDSampler) -> float + + Return the current minimum separation. + + +.. py:method:: PDSampler.SampleBox + + .. code-block:: text + + SampleBox(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], halfDim: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[list[float]] + + Return points sampled from the specified box volume. + + +.. py:method:: PDSampler.SampleCylinderX + + .. code-block:: text + + SampleCylinderX(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from an X-aligned cylindrical volume. + + +.. py:method:: PDSampler.SampleCylinderY + + .. code-block:: text + + SampleCylinderY(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from a Y-aligned cylindrical volume. + + +.. py:method:: PDSampler.SampleCylinderZ + + .. code-block:: text + + SampleCylinderZ(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from a Z-aligned cylindrical volume. + + +.. py:method:: PDSampler.SampleSphere + + .. code-block:: text + + SampleSphere(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from the specified spherical volume. + + +.. py:method:: PDSampler.SetRandomEngineSeed + + .. code-block:: text + + SetRandomEngineSeed(self: deme._deme.PDSampler, seed: typing.SupportsInt | typing.SupportsIndex) -> None + + Seed the random engine so Poisson-disk samples can be reproduced. + + +.. py:method:: PDSampler.SetSeparation + + .. code-block:: text + + SetSeparation(self: deme._deme.PDSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the minimum separation used by subsequent samples. + + + +SPATIAL_DIR +----------- + +.. py:class:: SPATIAL_DIR + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + +.. py:method:: SPATIAL_DIR.__init__ + + .. code-block:: text + + __init__(self: deme._deme.SPATIAL_DIR, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: SPATIAL_DIR.NONE + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + +.. py:attribute:: SPATIAL_DIR.X + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + +.. py:attribute:: SPATIAL_DIR.Y + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + +.. py:attribute:: SPATIAL_DIR.Z + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + + +TIME_INTEGRATOR +--------------- + +.. py:class:: TIME_INTEGRATOR + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG + +.. py:method:: TIME_INTEGRATOR.__init__ + + .. code-block:: text + + __init__(self: deme._deme.TIME_INTEGRATOR, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: TIME_INTEGRATOR.CENTERED_DIFFERENCE + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG + +.. py:attribute:: TIME_INTEGRATOR.CHUNG + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG + +.. py:attribute:: TIME_INTEGRATOR.EXTENDED_TAYLOR + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG + +.. py:attribute:: TIME_INTEGRATOR.FORWARD_EULER + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG + + +Tracker +------- + +.. py:class:: Tracker + + .. code-block:: text + + Access and modify owners associated with a tracked batch or object. + + Create trackers with ``DEMSolver.Track`` during setup. Most state queries and + updates require the solver to be initialized and synchronized. ``offset`` + selects an owner within the tracked collection, starting at zero. Keep the + parent solver alive while using a tracker. + + Fixed-size CUDA exchange methods validate ranges, capacities, and pointer metadata by default. Pass + ``validate=False`` only when those preconditions are guaranteed and avoiding validation overhead matters. Required + device routing and documented transformations, including quaternion normalization, still apply. + +.. py:method:: Tracker.__init__ + + .. code-block:: text + + __init__(self: deme._deme.Tracker, solver: deme::DEMSolver) -> None + + Create a tracker associated with ``solver``. Prefer ``DEMSolver.Track`` so the tracked owner range is configured correctly. + + +.. py:method:: Tracker.AddAcc + + .. code-block:: text + + AddAcc(*args, **kwargs) + Overloaded function. + + 1. AddAcc(self: deme._deme.Tracker, acc: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Add an extra acc to the tracked body, for the next time step. Note if the user intends to add a persistent external force, then using family prescription is the better method. + + 2. AddAcc(self: deme._deme.Tracker, acc: collections.abc.Sequence[float3]) -> None + + Add an extra acc to consecutive tracked objects, (only) for the next time step. Note if the user intends to add a persistent external force, then using family prescription is the better method. + + +.. py:method:: Tracker.AddAccFromDevice + + .. code-block:: text + + AddAccFromDevice(self: deme._deme.Tracker, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously queue one global-frame linear acceleration per tracked owner from CUDA memory. + + ``source`` is an integer address containing one CUDA ``float3`` per tracked owner in tracker order. Remote input is + copied from ``source_device`` to the dynamic-worker device before unpacking. Values replace any previously queued + next-step linear-acceleration contribution. Contact acceleration is accumulated on top during the next + force/integration step, gravity is applied separately, and the queued contribution is consumed after one step. Pass + ``validate=False`` only when the range and pointer metadata are known to be valid. The source buffer may be reused when + this call returns. + + +.. py:method:: Tracker.AddAngAcc + + .. code-block:: text + + AddAngAcc(*args, **kwargs) + Overloaded function. + + 1. AddAngAcc(self: deme._deme.Tracker, angAcc: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Add an extra angular acceleration to the tracked body, for the next time step. Note if the user intends to add a persistent external torque, then using family prescription is the better method. + + 2. AddAngAcc(self: deme._deme.Tracker, angAcc: collections.abc.Sequence[float3]) -> None + + Add an extra angular acceleration to consecutive tracked objects, (only) for the next time step. Note if the user intends to add a persistent external torque, then using family prescription is the better method. + + +.. py:method:: Tracker.AddAngAccFromDevice + + .. code-block:: text + + AddAngAccFromDevice(self: deme._deme.Tracker, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously queue one local-frame angular acceleration per tracked owner from CUDA memory. + + ``source`` is an integer address containing one CUDA ``float3`` per tracked owner in tracker order. Remote input is + copied from ``source_device`` to the dynamic-worker device before unpacking. Values use each owner's local principal- + axis frame and replace any previously queued next-step angular-acceleration contribution. Contact angular acceleration + is accumulated on top during the next force/integration step, and the queued contribution is consumed after one step. + Pass ``validate=False`` only when the range and pointer metadata are known to be valid. The source buffer may be reused + when this call returns. + + +.. py:method:: Tracker.AngularVelocitiesGlobal + + .. code-block:: text + + AngularVelocitiesGlobal(self: deme._deme.Tracker) -> list[list[float]] + + Return global-frame angular velocities for all tracked owners. + + +.. py:method:: Tracker.AngularVelocitiesGlobalToDevice + + .. code-block:: text + + AngularVelocitiesGlobalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy all tracked global-frame angular velocities to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.AngularVelocitiesLocal + + .. code-block:: text + + AngularVelocitiesLocal(self: deme._deme.Tracker) -> list[list[float]] + + Return local principal-axis-frame angular velocities for all tracked owners. + + +.. py:method:: Tracker.AngularVelocitiesLocalToDevice + + .. code-block:: text + + AngularVelocitiesLocalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy all tracked local-frame angular velocities to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.AngVelGlobal + + .. code-block:: text + + AngVelGlobal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Return the global-frame angular velocity of the owner at ``offset``. + + +.. py:method:: Tracker.AngVelLocal + + .. code-block:: text + + AngVelLocal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Return the angular velocity of the owner at ``offset`` in its local principal-axis frame. Rotate it by ``OriQ(offset)`` to obtain the global-frame value. + + +.. py:method:: Tracker.ContactAcc + + .. code-block:: text + + ContactAcc(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Get the a portion of the acceleration of this tracked object, that is the result of its contact with other simulation entities. In most cases, this means excluding the gravitational acceleration. The acceleration is in global frame. + + +.. py:method:: Tracker.ContactAccelerations + + .. code-block:: text + + ContactAccelerations(self: deme._deme.Tracker) -> list[list[float]] + + Get the acceleration experienced by all objects tracked by this tracker, that is the result of their contact with other simulation entities. The acceleration is in global frame. In most cases, this means excluding the gravitational acceleration. The acceleration is in global frame. + + +.. py:method:: Tracker.ContactAccelerationsToDevice + + .. code-block:: text + + ContactAccelerationsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy all tracked global-frame contact accelerations to caller-owned CUDA ``float3`` storage. Gravity and other non-contact acceleration are excluded. + + +.. py:method:: Tracker.ContactAngAccGlobal + + .. code-block:: text + + ContactAngAccGlobal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Get the a portion of the angular acceleration of this tracked object, that is the result of its contact with other simulation entities. The acceleration is in this object's global frame. + + +.. py:method:: Tracker.ContactAngAccLocal + + .. code-block:: text + + ContactAngAccLocal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Get the a portion of the angular acceleration of this tracked object, that is the result of its contact with other simulation entities. The acceleration is in this object's local frame. + + +.. py:method:: Tracker.ContactAngularAccelerationsGlobal + + .. code-block:: text + + ContactAngularAccelerationsGlobal(self: deme._deme.Tracker) -> list[list[float]] + + Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of their contact with other simulation entities. The acceleration is in this object's global frame. + + +.. py:method:: Tracker.ContactAngularAccelerationsGlobalToDevice + + .. code-block:: text + + ContactAngularAccelerationsGlobalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy all tracked global-frame contact angular accelerations to caller-owned CUDA ``float3`` storage. + + +.. py:method:: Tracker.ContactAngularAccelerationsLocal + + .. code-block:: text + + ContactAngularAccelerationsLocal(self: deme._deme.Tracker) -> list[list[float]] + + Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of their contact with other simulation entities. The acceleration is in this object's local frame. + + +.. py:method:: Tracker.ContactAngularAccelerationsLocalToDevice + + .. code-block:: text + + ContactAngularAccelerationsLocalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy all tracked local-frame contact angular accelerations to caller-owned CUDA ``float3`` storage. + + +.. py:method:: Tracker.ContactWrenches + + .. code-block:: text + + ContactWrenches(self: deme._deme.Tracker) -> tuple[list[float3], list[float3]] + + Return ``(forces, torques)`` with one reduced contact wrench per tracked owner. + + Both tuple elements preserve tracker owner order and contain ``float3`` values in the global frame. Torque is measured + about each owner's current DEME position and includes both force-generated moments and force-model-only torque. Owners + without recorded contact receive zero. This reads current dT force records without triggering contact detection or + force evaluation, so callers must advance/synchronize the simulation as needed. Contact recording must remain enabled. + + +.. py:method:: Tracker.ContactWrenchesToDevice + + .. code-block:: text + + ContactWrenchesToDevice(self: deme._deme.Tracker, force_destination: typing.SupportsInt | typing.SupportsIndex, torque_destination: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously write one reduced contact wrench per tracked owner to CUDA ``float3`` buffers. + + Outputs preserve tracker owner order. Force and torque are global-frame resultants; torque is measured about each + owner's current DEME position and includes force-generated moments and force-model-only torque. Owners without contact + receive zero. ``force_destination`` and ``torque_destination`` are integer addresses of writable CUDA ``float3`` + storage. ``capacity`` is the element capacity of each buffer and must cover every tracked owner. Both buffers must + belong to logical CUDA ``device``; CUDA selects the available inter-device transfer route for remote output. The call + is synchronous, so both buffers may be consumed when it returns. This reads current recorded dT forces and does not + trigger contact detection or force evaluation. Contact recording must remain enabled. + + +.. py:method:: Tracker.FamiliesToDevice + + .. code-block:: text + + FamiliesToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy all tracked family numbers to caller-owned CUDA ``uint32`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.GetContactClumps + + .. code-block:: text + + GetContactClumps(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[int] + + Get the clumps that are in contact with this tracked owner as a vector. + + +.. py:method:: Tracker.GetContactForces + + .. code-block:: text + + GetContactForces(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Get all contact forces that concern this tracked object. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndGlobalTorque + + .. code-block:: text + + GetContactForcesAndGlobalTorque(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3], offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Get all contact forces and global torques that concern this tracked object. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndGlobalTorqueForAll + + .. code-block:: text + + GetContactForcesAndGlobalTorqueForAll(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3]) -> int + + Get all contact forces and global torques that concern all objects tracked by this tracker. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndGlobalTorqueForAllToDevice + + .. code-block:: text + + GetContactForcesAndGlobalTorqueForAllToDevice(self: deme._deme.Tracker, points: typing.SupportsInt | typing.SupportsIndex, forces: typing.SupportsInt | typing.SupportsIndex, torques: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> int + + Synchronously compact contact points, forces, and global-frame extra torques into caller-owned CUDA ``float3`` arrays. Capacity must cover the total recorded-contact count; the return value is the number of valid rows. + + +.. py:method:: Tracker.GetContactForcesAndLocalTorque + + .. code-block:: text + + GetContactForcesAndLocalTorque(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3], offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Get all contact forces and local torques that concern this tracked object. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndLocalTorqueForAll + + .. code-block:: text + + GetContactForcesAndLocalTorqueForAll(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3]) -> int + + Get all contact forces and local torques that concern all objects tracked by this tracker. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndLocalTorqueForAllToDevice + + .. code-block:: text + + GetContactForcesAndLocalTorqueForAllToDevice(self: deme._deme.Tracker, points: typing.SupportsInt | typing.SupportsIndex, forces: typing.SupportsInt | typing.SupportsIndex, torques: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> int + + Synchronously compact contact points, forces, and owner-local extra torques into caller-owned CUDA ``float3`` arrays. Capacity must cover the total recorded-contact count; the return value is the number of valid rows. + + +.. py:method:: Tracker.GetContactForcesForAll + + .. code-block:: text + + GetContactForcesForAll(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3]) -> int + + Get all contact forces that concern all objects tracked by this tracker. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesForAllToDevice + + .. code-block:: text + + GetContactForcesForAllToDevice(self: deme._deme.Tracker, points: typing.SupportsInt | typing.SupportsIndex, forces: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> int + + + Synchronously compact contact points and forces for all tracked owners into CUDA ``float3`` arrays. + + Each pointer must address ``capacity`` elements on logical CUDA ``device``. + Capacity must cover the simulation's total recorded-contact count. The return + value is the number of valid compacted rows. Contact force recording must be + enabled. + + +.. py:method:: Tracker.GetFamilies + + .. code-block:: text + + GetFamilies(self: deme._deme.Tracker) -> list[int] + + Get the family numbers of all the tracked object. + + +.. py:method:: Tracker.GetFamily + + .. code-block:: text + + GetFamily(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Get the family number of the tracked object. + + +.. py:method:: Tracker.GetMesh + + .. code-block:: text + + GetMesh(self: deme._deme.Tracker) -> deme::DEMMesh + + Get a handle for the mesh this tracker is tracking. + + +.. py:method:: Tracker.GetMeshNodesGlobal + + .. code-block:: text + + GetMeshNodesGlobal(self: deme._deme.Tracker) -> list[float3] + + Get the current locations of all the nodes in the mesh being tracked. + + +.. py:method:: Tracker.GetOwnerID + + .. code-block:: text + + GetOwnerID(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Return the simulation-wide owner ID at ``offset`` within this tracker. + + +.. py:method:: Tracker.GetOwnerIDs + + .. code-block:: text + + GetOwnerIDs(self: deme._deme.Tracker) -> list[int] + + Return simulation-wide owner IDs for every owner covered by this tracker. + + +.. py:method:: Tracker.GetOwnerWildcardValue + + .. code-block:: text + + GetOwnerWildcardValue(self: deme._deme.Tracker, name: str, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> float + + Get the owner's wildcard value. + + +.. py:method:: Tracker.GetOwnerWildcardValues + + .. code-block:: text + + GetOwnerWildcardValues(self: deme._deme.Tracker, name: str) -> list[float] + + Get the owner wildcard values for all the owners entities tracked by this tracker. + + +.. py:method:: Tracker.Mass + + .. code-block:: text + + Mass(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> float + + Get the mass of the tracked object. + + +.. py:method:: Tracker.Masses + + .. code-block:: text + + Masses(self: deme._deme.Tracker) -> list[float] + + Get the masses of all the tracked objects. + + +.. py:method:: Tracker.MassesToDevice + + .. code-block:: text + + MassesToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy all tracked masses to caller-owned CUDA ``float32`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.MOI + + .. code-block:: text + + MOI(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Get the moment of inertia (in principal axis frame) of the tracked object. + + +.. py:method:: Tracker.MOIs + + .. code-block:: text + + MOIs(self: deme._deme.Tracker) -> list[list[float]] + + Get the moment of inertia (in principal axis frame) of all the tracked objects. + + +.. py:method:: Tracker.MOIsToDevice + + .. code-block:: text + + MOIsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy all tracked principal moments of inertia to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.OrientationQuaternions + + .. code-block:: text + + OrientationQuaternions(self: deme._deme.Tracker) -> list[list[float]] + + Return local-to-global orientation quaternions for all tracked owners. Each quaternion uses Python ordering ``(x, y, z, w)``. + + +.. py:method:: Tracker.OrientationQuaternionsToDevice + + .. code-block:: text + + OrientationQuaternionsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy all tracked local-to-global quaternions to caller-owned CUDA ``float4`` storage in ``(x, y, z, w)`` order. ``capacity`` is in elements. + + +.. py:method:: Tracker.OriQ + + .. code-block:: text + + OriQ(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Return the quaternion rotating the owner-local frame to the global frame. Python ordering is ``(x, y, z, w)`` (Chrono ``(e1, e2, e3, e0)``). + + +.. py:method:: Tracker.OwnerWildcardValuesToDevice + + .. code-block:: text + + OwnerWildcardValuesToDevice(self: deme._deme.Tracker, name: str, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously copy the named ``float32`` owner wildcard for every tracked owner to caller-owned CUDA storage. ``capacity`` is in owner elements. + + +.. py:method:: Tracker.Pos + + .. code-block:: text + + Pos(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Return the global-frame center-of-mass position of the tracked owner at ``offset``. + + +.. py:method:: Tracker.Positions + + .. code-block:: text + + Positions(self: deme._deme.Tracker) -> list[list[float]] + + Return global-frame center-of-mass positions for all owners covered by this tracker. + + +.. py:method:: Tracker.PositionsToDevice + + .. code-block:: text + + PositionsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + + Synchronously copy all tracked global positions to caller-owned CUDA memory. + + ``pointer`` is an integer address to writable, CUDA-accessible ``float3`` + storage on logical CUDA ``device``. ``capacity`` is measured in ``float3`` + elements and must cover every owner in this tracker. The caller owns the + allocation and must keep it alive through the call. + + +.. py:method:: Tracker.SetAngularVelocitiesFromDevice + + .. code-block:: text + + SetAngularVelocitiesFromDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously set every tracked local-frame angular velocity from CUDA ``float3`` storage. The buffer must contain one element per tracked owner; remote input is copied to the dynamic-worker device before unpacking. + + +.. py:method:: Tracker.SetAngularVelocitiesGlobalFromDevice + + .. code-block:: text + + SetAngularVelocitiesGlobalFromDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously set every tracked global angular velocity from CUDA ``float3`` storage. Each value is converted to the owner's local principal-axis frame using its orientation at call time. The buffer must contain one element per tracked owner; remote input is copied to the dynamic-worker device before unpacking. + + +.. py:method:: Tracker.SetAngVel + + .. code-block:: text + + SetAngVel(*args, **kwargs) + Overloaded function. + + 1. SetAngVel(self: deme._deme.Tracker, angVel: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Set the angular velocity of this tracked object in its own local coordinate system. + + 2. SetAngVel(self: deme._deme.Tracker, angVel: collections.abc.Sequence[float3]) -> None + + Set the angular velocity of consecutive tracked objects in their own local coordinate systems. + + +.. py:method:: Tracker.SetFamily + + .. code-block:: text + + SetFamily(*args, **kwargs) + Overloaded function. + + 1. SetFamily(self: deme._deme.Tracker, fam_num: typing.SupportsInt | typing.SupportsIndex) -> None + + Change the family numbers of all the entities tracked by this tracker. + + 2. SetFamily(self: deme._deme.Tracker, fam_num: typing.SupportsInt | typing.SupportsIndex, offset: typing.SupportsInt | typing.SupportsIndex) -> None + + Change the family number of one entities tracked by this tracker. + + +.. py:method:: Tracker.SetOrientationQuaternionsFromDevice + + .. code-block:: text + + SetOrientationQuaternionsFromDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None + + Synchronously set every tracked local-to-global quaternion from CUDA ``float4`` storage in ``(x, y, z, w)`` order. With validation enabled, non-finite and zero-length inputs are rejected; finite, nonzero inputs are always normalized. The buffer must contain one element per tracked owner; remote input is copied to the dynamic-worker device before validation and unpacking. + + +.. py:method:: Tracker.SetOriQ + + .. code-block:: text + + SetOriQ(*args, **kwargs) + Overloaded function. + + 1. SetOriQ(self: deme._deme.Tracker, oriQ: float4, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Set the quaternion which represents the orientation of this tracked object's coordinate system. + + 2. SetOriQ(self: deme._deme.Tracker, oriQ: collections.abc.Sequence[float4]) -> None + + Set the quaternion which represents the orientation of consecutive tracked objects' coordinate systems. + + +.. py:method:: Tracker.SetOwnerWildcardValue + + .. code-block:: text + + SetOwnerWildcardValue(self: deme._deme.Tracker, name: str, wc: typing.SupportsFloat | typing.SupportsIndex, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Set a wildcard value of the owner this tracker is tracking. + + +.. py:method:: Tracker.SetOwnerWildcardValues + + .. code-block:: text + + SetOwnerWildcardValues(self: deme._deme.Tracker, name: str, wc: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set a wildcard value of the owner this tracker is tracking. + + +.. py:method:: Tracker.SetPos + + .. code-block:: text + + SetPos(*args, **kwargs) + Overloaded function. + + 1. SetPos(self: deme._deme.Tracker, pos: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Set the position of this tracked object. + + 2. SetPos(self: deme._deme.Tracker, pos: collections.abc.Sequence[float3]) -> None - ANG_VEL + Set the positions of consecutive tracked objects. - ABS_ACC - ACC +.. py:method:: Tracker.SetPositionsFromDevice - ANG_ACC + .. code-block:: text - FAMILY + SetPositionsFromDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None - MAT + Synchronously set every tracked global position from CUDA ``float3`` storage. The buffer must contain one element per tracked owner; remote input is copied to the dynamic-worker device before unpacking. - OWNER_WILDCARD -.. py:attribute:: OUTPUT_CONTENT.XYZ +.. py:method:: Tracker.SetVel .. code-block:: text - Members: + SetVel(*args, **kwargs) + Overloaded function. - XYZ + 1. SetVel(self: deme._deme.Tracker, vel: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None - QUAT + Set the velocity of this tracked object in global frame. - ABSV + 2. SetVel(self: deme._deme.Tracker, vel: collections.abc.Sequence[float3]) -> None - VEL + Set the velocity of consecutive tracked objects in global frame. - ANG_VEL - ABS_ACC +.. py:method:: Tracker.SetVelocitiesFromDevice - ACC + .. code-block:: text - ANG_ACC + SetVelocitiesFromDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None - FAMILY + Synchronously set every tracked global linear velocity from CUDA ``float3`` storage. The buffer must contain one element per tracked owner; remote input is copied to the dynamic-worker device before unpacking. - MAT - OWNER_WILDCARD +.. py:method:: Tracker.UpdateMesh + .. code-block:: text -OUTPUT_FORMAT -------------- + UpdateMesh(self: deme._deme.Tracker, new_nodes: collections.abc.Sequence[float3], update_patch_centers: bool = True) -> None -.. py:class:: OUTPUT_FORMAT + Replace local mesh node coordinates. update_patch_centers refreshes automatic centers from all accumulated deformation; false preserves current centers. It has no effect for user-supplied centers. Automatic single-patch centers remain at the local origin. + + +.. py:method:: Tracker.UpdateMeshByIncrement .. code-block:: text - Members: + UpdateMeshByIncrement(self: deme._deme.Tracker, deformation: collections.abc.Sequence[float3], update_patch_centers: bool = True) -> None - CSV + Add local mesh node displacements. update_patch_centers refreshes automatic centers from all accumulated deformation; false preserves current centers. It has no effect for user-supplied centers. Automatic single-patch centers remain at the local origin. - BINARY -.. py:method:: OUTPUT_FORMAT.__init__ +.. py:method:: Tracker.UpdateMeshByIncrementFromDevice .. code-block:: text - __init__(self: deme._deme.OUTPUT_FORMAT, value: typing.SupportsInt | typing.SupportsIndex) -> None + UpdateMeshByIncrementFromDevice(self: deme._deme.Tracker, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True, update_patch_centers: bool = True) -> None + Synchronous local-node displacement increment: one float3 per node, fixed topology. Finish producing source before calling. validate checks pointer and finite values. update_patch_centers refreshes automatic centers only and has no effect for user-supplied centers. Single-patch automatic centers stay at the local origin. A later enabled refresh includes skipped deformation. Tracker GetMesh/GetMeshNodesGlobal synchronize the lazy CPU geometry cache. -.. py:attribute:: OUTPUT_FORMAT.BINARY + +.. py:method:: Tracker.UpdateMeshFromDevice .. code-block:: text - Members: + UpdateMeshFromDevice(self: deme._deme.Tracker, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True, update_patch_centers: bool = True) -> None - CSV + Synchronous local-node device update: one float3 per node, fixed topology. Finish producing source before calling. validate checks pointer and finite values. update_patch_centers refreshes automatic centers only and has no effect for user-supplied centers. Single-patch automatic centers stay at the local origin. Tracker GetMesh/GetMeshNodesGlobal synchronize the lazy CPU geometry cache. - BINARY -.. py:attribute:: OUTPUT_FORMAT.CSV +.. py:method:: Tracker.UpdateMeshPatchLocations .. code-block:: text - Members: - - CSV - - BINARY + UpdateMeshPatchLocations(self: deme._deme.Tracker, centers: collections.abc.Sequence[float3]) -> None + Set one local center per patch at runtime and select user-managed centers. -OWNER_TYPE ----------- -.. py:class:: OWNER_TYPE +.. py:method:: Tracker.UpdateMeshPatchLocationsFromDevice .. code-block:: text - Members: - - CLUMP + UpdateMeshPatchLocationsFromDevice(self: deme._deme.Tracker, source: typing.SupportsInt | typing.SupportsIndex, source_device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None - ANALYTICAL + Synchronously set user-managed local patch centers from one CUDA float3 per patch. - MESH -.. py:method:: OWNER_TYPE.__init__ +.. py:method:: Tracker.UseAutomaticMeshPatchLocations .. code-block:: text - __init__(self: deme._deme.OWNER_TYPE, value: typing.SupportsInt | typing.SupportsIndex) -> None + UseAutomaticMeshPatchLocations(self: deme._deme.Tracker) -> None + Resume automatic centers and immediately recompute from current geometry. -.. py:attribute:: OWNER_TYPE.ANALYTICAL - .. code-block:: text +.. py:method:: Tracker.Vel - Members: + .. code-block:: text - CLUMP + Vel(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] - ANALYTICAL + Return the global-frame linear velocity of the owner at ``offset``. - MESH -.. py:attribute:: OWNER_TYPE.CLUMP +.. py:method:: Tracker.Velocities .. code-block:: text - Members: - - CLUMP + Velocities(self: deme._deme.Tracker) -> list[list[float]] - ANALYTICAL + Return global-frame linear velocities for all tracked owners. - MESH -.. py:attribute:: OWNER_TYPE.MESH +.. py:method:: Tracker.VelocitiesToDevice .. code-block:: text - Members: - - CLUMP + VelocitiesToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex, validate: bool = True) -> None - ANALYTICAL + Synchronously copy all tracked global linear velocities to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. - MESH -PDSampler ---------- +VisualizationFrame +------------------ -.. py:class:: PDSampler +.. py:class:: VisualizationFrame .. code-block:: text - Poisson-disk volumetric sampler enforcing a minimum point separation. + No class description is currently available. -.. py:method:: PDSampler.__init__ +.. py:method:: VisualizationFrame.__init__ .. code-block:: text - __init__(self: deme._deme.PDSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None - - Create a sampler with the requested minimum separation. + __init__(self: deme._deme.VisualizationFrame) -> None -.. py:method:: PDSampler.GetSeparation +.. py:attribute:: VisualizationFrame.families .. code-block:: text - GetSeparation(self: deme._deme.PDSampler) -> float - - Return the current minimum separation. - + Bound attribute. -.. py:method:: PDSampler.SampleBox +.. py:attribute:: VisualizationFrame.orientations .. code-block:: text - SampleBox(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], halfDim: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[list[float]] + Bound attribute. - Return points sampled from the specified box volume. +.. py:attribute:: VisualizationFrame.positions + .. code-block:: text -.. py:method:: PDSampler.SampleCylinderX + Bound attribute. + +.. py:attribute:: VisualizationFrame.revision .. code-block:: text - SampleCylinderX(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + Bound attribute. - Return points sampled from an X-aligned cylindrical volume. +.. py:attribute:: VisualizationFrame.simulation_time + .. code-block:: text -.. py:method:: PDSampler.SampleCylinderY + Bound attribute. + +.. py:attribute:: VisualizationFrame.velocities .. code-block:: text - SampleCylinderY(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + Bound attribute. - Return points sampled from a Y-aligned cylindrical volume. +VisualizationScene +------------------ -.. py:method:: PDSampler.SampleCylinderZ +.. py:class:: VisualizationScene .. code-block:: text - SampleCylinderZ(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + No class description is currently available. - Return points sampled from a Z-aligned cylindrical volume. +.. py:method:: VisualizationScene.__init__ + .. code-block:: text -.. py:method:: PDSampler.SampleSphere + Initialize self. See help(type(self)) for accurate signature. + + +.. py:attribute:: VisualizationScene.revision .. code-block:: text - SampleSphere(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + Bound attribute. - Return points sampled from the specified spherical volume. +.. py:attribute:: VisualizationScene.spheres + + .. code-block:: text + Bound attribute. -.. py:method:: PDSampler.SetRandomEngineSeed +.. py:attribute:: VisualizationScene.triangles .. code-block:: text - SetRandomEngineSeed(self: deme._deme.PDSampler, seed: typing.SupportsInt | typing.SupportsIndex) -> None + Bound attribute. - Seed the random engine so Poisson-disk samples can be reproduced. +VisualizationSphere +------------------- -.. py:method:: PDSampler.SetSeparation +.. py:class:: VisualizationSphere .. code-block:: text - SetSeparation(self: deme._deme.PDSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + No class description is currently available. - Set the minimum separation used by subsequent samples. +.. py:method:: VisualizationSphere.__init__ + .. code-block:: text + Initialize self. See help(type(self)) for accurate signature. -SPATIAL_DIR ------------ -.. py:class:: SPATIAL_DIR +.. py:attribute:: VisualizationSphere.offset .. code-block:: text - Members: - - X + Bound attribute. - Y +.. py:attribute:: VisualizationSphere.owner - Z + .. code-block:: text - NONE + Bound attribute. -.. py:method:: SPATIAL_DIR.__init__ +.. py:attribute:: VisualizationSphere.radius .. code-block:: text - __init__(self: deme._deme.SPATIAL_DIR, value: typing.SupportsInt | typing.SupportsIndex) -> None + Bound attribute. -.. py:attribute:: SPATIAL_DIR.NONE +VisualizationTriangle +--------------------- + +.. py:class:: VisualizationTriangle .. code-block:: text - Members: + No class description is currently available. - X +.. py:method:: VisualizationTriangle.__init__ - Y + .. code-block:: text - Z + Initialize self. See help(type(self)) for accurate signature. - NONE -.. py:attribute:: SPATIAL_DIR.X +.. py:attribute:: VisualizationTriangle.a .. code-block:: text - Members: - - X + Bound attribute. - Y +.. py:attribute:: VisualizationTriangle.b - Z + .. code-block:: text - NONE + Bound attribute. -.. py:attribute:: SPATIAL_DIR.Y +.. py:attribute:: VisualizationTriangle.c .. code-block:: text - Members: + Bound attribute. - X +.. py:attribute:: VisualizationTriangle.owner - Y + .. code-block:: text - Z + Bound attribute. - NONE -.. py:attribute:: SPATIAL_DIR.Z +VisualizerColor +--------------- + +.. py:class:: VisualizerColor .. code-block:: text - Members: + RGBA color used by DEMVisualizer. - X +.. py:method:: VisualizerColor.__init__ - Y + .. code-block:: text - Z + __init__(self: deme._deme.VisualizerColor, r: typing.SupportsInt | typing.SupportsIndex = 255, g: typing.SupportsInt | typing.SupportsIndex = 255, b: typing.SupportsInt | typing.SupportsIndex = 255, a: typing.SupportsInt | typing.SupportsIndex = 255) -> None - NONE +.. py:attribute:: VisualizerColor.a -TIME_INTEGRATOR ---------------- + .. code-block:: text -.. py:class:: TIME_INTEGRATOR + Bound attribute. - .. code-block:: text +.. py:attribute:: VisualizerColor.b - Members: + .. code-block:: text - FORWARD_EULER + Bound attribute. - CENTERED_DIFFERENCE +.. py:attribute:: VisualizerColor.g - EXTENDED_TAYLOR + .. code-block:: text - CHUNG + Bound attribute. -.. py:method:: TIME_INTEGRATOR.__init__ +.. py:attribute:: VisualizerColor.r .. code-block:: text - __init__(self: deme._deme.TIME_INTEGRATOR, value: typing.SupportsInt | typing.SupportsIndex) -> None + Bound attribute. -.. py:attribute:: TIME_INTEGRATOR.CENTERED_DIFFERENCE +VisualizerColorMode +------------------- + +.. py:class:: VisualizerColorMode .. code-block:: text Members: - FORWARD_EULER + FAMILY - CENTERED_DIFFERENCE + HEIGHT - EXTENDED_TAYLOR + SPEED - CHUNG +.. py:method:: VisualizerColorMode.__init__ -.. py:attribute:: TIME_INTEGRATOR.CHUNG + .. code-block:: text + + __init__(self: deme._deme.VisualizerColorMode, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: VisualizerColorMode.FAMILY .. code-block:: text Members: - FORWARD_EULER - - CENTERED_DIFFERENCE + FAMILY - EXTENDED_TAYLOR + HEIGHT - CHUNG + SPEED -.. py:attribute:: TIME_INTEGRATOR.EXTENDED_TAYLOR +.. py:attribute:: VisualizerColorMode.HEIGHT .. code-block:: text Members: - FORWARD_EULER - - CENTERED_DIFFERENCE + FAMILY - EXTENDED_TAYLOR + HEIGHT - CHUNG + SPEED -.. py:attribute:: TIME_INTEGRATOR.FORWARD_EULER +.. py:attribute:: VisualizerColorMode.SPEED .. code-block:: text Members: - FORWARD_EULER - - CENTERED_DIFFERENCE + FAMILY - EXTENDED_TAYLOR + HEIGHT - CHUNG + SPEED diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index f56ae492..768db8e5 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -52,6 +52,16 @@ Build with compilers compatible with the target Conda environment. A wheel built against a newer system ``libstdc++`` may import on the build host but fail inside another environment. +Cannot open or read a JIT source file +------------------------------------- + +JIT source-loading errors report the attempted absolute path. Check that the +runtime kernel/header resources exist there and are readable. On Windows, also +check which ``DEMERuntimeDataHelper.dll`` the application loads: a stale copy can +point to resources from another build. Refresh the helper library and runtime +resources together. Files that exist but contain older source code are not +identified by the file-read check; see the stale-kernel guidance below. + Stale runtime kernels --------------------- @@ -71,6 +81,19 @@ startup work. Set ``DEME_PERSISTENT_JITIFY_CACHE`` to a writable cache-file path export DEME_PERSISTENT_JITIFY_CACHE="$HOME/.cache/deme/jitify_header_cache.bin" mkdir -p "$(dirname "$DEME_PERSISTENT_JITIFY_CACHE")" +On Windows, set the variable in the PowerShell session that launches the executable: + +.. code-block:: powershell + + $cacheDirectory = Join-Path $env:LOCALAPPDATA "deme" + New-Item -ItemType Directory -Force -Path $cacheDirectory | Out-Null + $env:DEME_PERSISTENT_JITIFY_CACHE = Join-Path $cacheDirectory "jitify_header_cache.bin" + +The first run still discovers headers and compiles kernels. Later fresh processes can reuse the saved headers when +compiling new kernels, while matching compiled kernels are reused separately through ``DEME_JIT_CACHE_DIR``. To measure +the header cache's benefit, keep its file but select an empty compiled-kernel cache for each comparison run. A fully +populated compiled-kernel cache can hide the header-discovery cost. + Values such as ``1``, ``true``, ``on``, and ``yes`` select an automatic temporary path. Values such as ``0``, ``false``, ``off``, and ``no``, as well as an unset variable, disable the persistent cache. The cache is opt-in because an automatic shared diff --git a/src/DEM/API.h b/src/DEM/API.h index 2bd8e2e6..db4f8a13 100644 --- a/src/DEM/API.h +++ b/src/DEM/API.h @@ -853,9 +853,42 @@ class DEMSolver { bool validate = true); /// @brief Rewrite the relative positions of the flattened triangle soup. - void SetTriNodeRelPos(size_t owner, size_t triID, const std::vector& new_nodes); + /// @param update_patch_centers Refresh automatic centers from all accumulated deformation (default true). + /// False preserves current centers. Has no effect for user-supplied centers, which are never overwritten. + /// Automatic single-patch centers remain at the local origin. + void SetTriNodeRelPos(size_t owner, + size_t triID, + const std::vector& new_nodes, + bool update_patch_centers = true); /// @brief Update the relative positions of the flattened triangle soup. - void UpdateTriNodeRelPos(size_t owner, size_t triID, const std::vector& updates); + /// @param update_patch_centers Refresh automatic centers from all accumulated deformation (default true). + /// False preserves current centers. Has no effect for user-supplied centers, which are never overwritten. + /// Automatic single-patch centers remain at the local origin. + void UpdateTriNodeRelPos(size_t owner, + size_t triID, + const std::vector& updates, + bool update_patch_centers = true); + /// Device-input counterparts of the local node setters. Supply one float3 per mesh node, in original order. + /// Calls are synchronous; finish producing source on its CUDA stream before calling. Topology is fixed. + /// validate=false skips pointer/finite-value checks. update_patch_centers refreshes automatic centers only: + /// one patch uses the local origin, multiple patches average triangle centroids. Explicit centers are preserved. + /// Device updates lazily synchronize CPU geometry through GetCachedMesh, tracker getters, or solver output. + void SetTriNodeRelPosFromDevice(bodyID_t owner, + const float3* source, + int source_device, + bool validate = true, + bool update_patch_centers = true); + void UpdateTriNodeRelPosFromDevice(bodyID_t owner, + const float3* source, + int source_device, + bool validate = true, + bool update_patch_centers = true); + /// Set one local center per patch at runtime and select user-managed centers (preserved during deformation). + void SetMeshPatchLocations(bodyID_t owner, const std::vector& centers); + /// Same semantics as SetMeshPatchLocations; source contains one float3 per patch. Synchronous device input. + void SetMeshPatchLocationsFromDevice(bodyID_t owner, const float3* source, int source_device, bool validate = true); + /// Resume automatic center management and immediately recompute centers from current geometry. + void UseAutomaticMeshPatchLocations(bodyID_t owner); /// @brief Get a handle for the mesh this tracker is tracking. /// @return Pointer to the mesh. std::shared_ptr& GetCachedMesh(bodyID_t ownerID); @@ -1668,6 +1701,8 @@ class DEMSolver { WriteAnalyticalFile(outfilename.string(), circumferential_resolution); } /// @brief Write all contact pairs to a file. + /// Contact data is captured before this call returns; formatting and disk writing then run asynchronously. + /// Subsequent dynamics calls may proceed without waiting for this contact output to finish. /// @details The outputted torque using this method is in global, rather than each object's local coordinate system. /// @param outfilename Output filename. /// @param force_thres Forces with magnitude smaller than this amount will not be outputted. diff --git a/src/DEM/APIPrivate.cpp b/src/DEM/APIPrivate.cpp index bcc22640..6eb2c90e 100644 --- a/src/DEM/APIPrivate.cpp +++ b/src/DEM/APIPrivate.cpp @@ -1492,8 +1492,9 @@ void DEMSolver::initializeGPUArrays() { // Analytical objects' initial stats m_input_ext_obj_family, // Meshed objects' initial stats - m_input_mesh_obj_family, m_input_mesh_obj_convex, m_input_mesh_obj_never_winner, m_mesh_facet_owner, - m_mesh_facet_patch, m_mesh_facet_neighbor1, m_mesh_facet_neighbor2, m_mesh_facet_neighbor3, m_mesh_facets, + cached_mesh_objs, m_input_mesh_obj_family, m_input_mesh_obj_convex, m_input_mesh_obj_never_winner, + m_mesh_facet_owner, m_mesh_facet_patch, m_mesh_facet_neighbor1, m_mesh_facet_neighbor2, + m_mesh_facet_neighbor3, m_mesh_facets, // Analytical obj physics properties m_ext_obj_comp_num, // Family mask @@ -1554,8 +1555,9 @@ void DEMSolver::updateClumpMeshArrays(size_t nOwners, // Analytical objects' initial stats m_input_ext_obj_family, // Meshed objects' initial stats - m_input_mesh_obj_family, m_input_mesh_obj_convex, m_input_mesh_obj_never_winner, m_mesh_facet_owner, - m_mesh_facet_patch, m_mesh_facet_neighbor1, m_mesh_facet_neighbor2, m_mesh_facet_neighbor3, m_mesh_facets, + cached_mesh_objs, m_input_mesh_obj_family, m_input_mesh_obj_convex, m_input_mesh_obj_never_winner, + m_mesh_facet_owner, m_mesh_facet_patch, m_mesh_facet_neighbor1, m_mesh_facet_neighbor2, + m_mesh_facet_neighbor3, m_mesh_facets, // Analytical obj physics properties m_ext_obj_comp_num, // Family mask @@ -1624,6 +1626,13 @@ void DEMSolver::migrateArrayDataToHost() { { ScopedCudaDevice device_scope(kT->streamInfo.device); kT->migrateDeviceModifiableInfoToHost(); + // Mesh deformation is device-major on dT. Update will reload both workers, so seed kT's host geometry + // from the synchronized dT cache instead of restoring its original initialization coordinates. + for (size_t t = 0; t < dT->relPosNode1.size(); ++t) { + kT->relPosNode1[t] = dT->relPosNode1[t]; + kT->relPosNode2[t] = dT->relPosNode2[t]; + kT->relPosNode3[t] = dT->relPosNode3[t]; + } } } diff --git a/src/DEM/APIPublic.cpp b/src/DEM/APIPublic.cpp index 59941493..f903fe9e 100644 --- a/src/DEM/APIPublic.cpp +++ b/src/DEM/APIPublic.cpp @@ -7,6 +7,7 @@ #include "API.h" #include "Defines.h" #include "utils/AnalyticalOutput.hpp" +#include "utils/ContactOutput.hpp" #include "utils/CombinedOwnerUtils.hpp" #include "utils/HostSideHelpers.hpp" #include "utils/MeshUtils.hpp" @@ -890,8 +891,7 @@ void DEMSolver::EnableMeshWearModel(bodyID_t ownerID, if (!findOwnerTriangleRange(ownerID, tri_start, tri_count)) { DEME_ERROR("Cannot determine triangle range for mesh owner %zu.", (size_t)ownerID); } - auto mesh_it = m_owner_mesh_map.find(ownerID); - auto& mesh = m_meshes.at(mesh_it->second); + auto& mesh = GetCachedMesh(ownerID); const auto& faces = mesh->GetIndicesVertexes(); if (faces.size() != tri_count) { DEME_ERROR("Wear model triangle count mismatch for owner %zu (range=%zu, mesh=%zu).", (size_t)ownerID, @@ -1293,60 +1293,126 @@ void DEMSolver::SetOwnerFamily(bodyID_t ownerID, unsigned int fam, bodyID_t n) { } } -void DEMSolver::SetTriNodeRelPos(size_t owner, size_t triID, const std::vector& new_nodes) { +// Host updates share the GPU deformation path, then eagerly refresh the cache for backward compatibility. +void DEMSolver::SetTriNodeRelPos(size_t owner, + size_t triID, + const std::vector& new_nodes, + bool update_patch_centers) { + assertSysInit("SetTriNodeRelPos"); WaitForPendingOutput(); - auto& mesh = m_meshes.at(m_owner_mesh_map.at(owner)); - if (mesh->GetNumNodes() != new_nodes.size()) { - DEME_ERROR( - "To deform a mesh, provided vector must have the same length as the number of nodes in mesh.\nThe mesh has " - "%zu nodes, yet the provided vector has length %zu.", - mesh->GetNumNodes(), new_nodes.size()); - } - // We actually modify the cached mesh... since it has implications in output - for (size_t i = 0; i < mesh->GetNumNodes(); i++) { - mesh->m_vertices[i] = new_nodes[i]; - } - std::vector new_triangles(mesh->GetNumTriangles()); - for (size_t i = 0; i < mesh->GetNumTriangles(); i++) { - new_triangles[i] = mesh->GetTriangle(i); - } ScopedCudaDevice device_scope(dT->streamInfo.device); - dT->setTriNodeRelPos(triID, new_triangles); - dT->solverFlags.willMeshDeform = true; + if (!dT->meshDeformation.count(owner)) + DEME_ERROR("Owner %zu is not an initialized mesh.", (size_t)owner); + auto& state = *dT->meshDeformation.at(owner); + if (triID != state.tri_start || new_nodes.size() != state.vertices.size()) { + DEME_ERROR("Mesh %zu expects triangle start %zu and %zu nodes; received start %zu and %zu nodes.", owner, + state.tri_start, state.vertices.size(), triID, new_nodes.size()); + } + state.prepareDevice(); + std::copy(new_nodes.begin(), new_nodes.end(), state.input.host()); + state.input.toDevice(0, new_nodes.size()); + dT->deformMesh(owner, state.input.data(), dT->streamInfo.device, false, true, update_patch_centers); + dT->synchronizeMeshCache(owner); +} + +void DEMSolver::UpdateTriNodeRelPos(size_t owner, + size_t triID, + const std::vector& updates, + bool update_patch_centers) { + assertSysInit("UpdateTriNodeRelPos"); + WaitForPendingOutput(); + ScopedCudaDevice device_scope(dT->streamInfo.device); + if (!dT->meshDeformation.count(owner)) + DEME_ERROR("Owner %zu is not an initialized mesh.", (size_t)owner); + auto& state = *dT->meshDeformation.at(owner); + if (triID != state.tri_start || updates.size() != state.vertices.size()) { + DEME_ERROR("Mesh %zu expects triangle start %zu and %zu nodes; received start %zu and %zu nodes.", owner, + state.tri_start, state.vertices.size(), triID, updates.size()); + } + state.prepareDevice(); + std::copy(updates.begin(), updates.end(), state.input.host()); + state.input.toDevice(0, updates.size()); + dT->deformMesh(owner, state.input.data(), dT->streamInfo.device, true, true, update_patch_centers); + dT->synchronizeMeshCache(owner); +} + +// Device-input setters leave the CPU cache dirty until a supported getter/output path observes it. +void DEMSolver::SetTriNodeRelPosFromDevice(bodyID_t owner, + const float3* source, + int source_device, + bool validate, + bool update_patch_centers) { + assertSysInit("SetTriNodeRelPosFromDevice"); + WaitForPendingOutput(); + ScopedCudaDevice device_scope(dT->streamInfo.device); + if (!dT->meshDeformation.count(owner)) + DEME_ERROR("Owner %zu is not an initialized mesh.", (size_t)owner); + dT->deformMesh(owner, source, source_device, false, validate, update_patch_centers); +} - // kT just receives update from dT, to avoid mem hazards - // kT->setTriNodeRelPos(triID, new_triangles); +void DEMSolver::UpdateTriNodeRelPosFromDevice(bodyID_t owner, + const float3* source, + int source_device, + bool validate, + bool update_patch_centers) { + assertSysInit("UpdateTriNodeRelPosFromDevice"); + WaitForPendingOutput(); + ScopedCudaDevice device_scope(dT->streamInfo.device); + if (!dT->meshDeformation.count(owner)) + DEME_ERROR("Owner %zu is not an initialized mesh.", (size_t)owner); + dT->deformMesh(owner, source, source_device, true, validate, update_patch_centers); } -void DEMSolver::UpdateTriNodeRelPos(size_t owner, size_t triID, const std::vector& updates) { + +// Runtime center setters select user-managed centers without altering geometry or patch membership. +void DEMSolver::SetMeshPatchLocations(bodyID_t owner, const std::vector& centers) { + assertSysInit("SetMeshPatchLocations"); WaitForPendingOutput(); - auto& mesh = m_meshes.at(m_owner_mesh_map.at(owner)); - if (mesh->GetNumNodes() != updates.size()) { - DEME_ERROR( - "To deform a mesh, provided vector must have the same length as the number of nodes in mesh.\nThe mesh has " - "%zu nodes, yet the provided vector has length %zu.", - mesh->GetNumNodes(), updates.size()); - } - // We actually modify the cached mesh... since it has implications in output - for (size_t i = 0; i < mesh->GetNumNodes(); i++) { - mesh->m_vertices[i] += updates[i]; - } - // No need to worry about RHR: that's taken care of at init - std::vector new_triangles(mesh->GetNumTriangles()); - for (size_t i = 0; i < mesh->GetNumTriangles(); i++) { - new_triangles[i] = mesh->GetTriangle(i); - } - // This is correct to use setTriNodeRelPos, as mesh is already modified in this method ScopedCudaDevice device_scope(dT->streamInfo.device); - dT->setTriNodeRelPos(triID, new_triangles); - dT->solverFlags.willMeshDeform = true; + if (!dT->meshDeformation.count(owner)) + DEME_ERROR("Owner %zu is not an initialized mesh.", (size_t)owner); + auto& state = *dT->meshDeformation.at(owner); + if (centers.size() != state.patch_offsets.size() - 1) { + DEME_ERROR("Mesh patch location update requires exactly %zu centers.", state.patch_offsets.size() - 1); + } + state.prepareDevice(); + std::copy(centers.begin(), centers.end(), state.input.host()); + state.input.toDevice(0, centers.size()); + dT->setMeshPatchLocations(owner, state.input.data(), dT->streamInfo.device, true); + dT->synchronizeMeshCache(owner); +} + +void DEMSolver::SetMeshPatchLocationsFromDevice(bodyID_t owner, + const float3* source, + int source_device, + bool validate) { + assertSysInit("SetMeshPatchLocationsFromDevice"); + WaitForPendingOutput(); + ScopedCudaDevice device_scope(dT->streamInfo.device); + if (!dT->meshDeformation.count(owner)) + DEME_ERROR("Owner %zu is not an initialized mesh.", (size_t)owner); + dT->setMeshPatchLocations(owner, source, source_device, validate); +} - // kT just receives update from dT, to avoid mem hazards - // kT->setTriNodeRelPos(triID, new_triangles); +// Returning to automatic management immediately incorporates every previous deformation. +void DEMSolver::UseAutomaticMeshPatchLocations(bodyID_t owner) { + assertSysInit("UseAutomaticMeshPatchLocations"); + WaitForPendingOutput(); + ScopedCudaDevice device_scope(dT->streamInfo.device); + if (!dT->meshDeformation.count(owner)) + DEME_ERROR("Owner %zu is not an initialized mesh.", (size_t)owner); + dT->meshDeformation.at(owner)->mesh->patch_locations_explicitly_set = false; + dT->refreshMeshPatchCenters(owner); + DEME_GPU_CALL(cudaStreamSynchronize(dT->streamInfo.stream)); + dT->synchronizeMeshCache(owner); } + std::shared_ptr& DEMSolver::GetCachedMesh(bodyID_t ownerID) { if (m_owner_mesh_map.find(ownerID) == m_owner_mesh_map.end()) { DEME_ERROR("Owner %zu is not a mesh, you therefore cannot retrive a handle to mesh using it.", (size_t)ownerID); } + WaitForPendingOutput(); + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->synchronizeMeshCache(ownerID); return m_meshes.at(m_owner_mesh_map.at(ownerID)); } std::vector DEMSolver::GetMeshNodesGlobal(bodyID_t ownerID) { @@ -1354,6 +1420,8 @@ std::vector DEMSolver::GetMeshNodesGlobal(bodyID_t ownerID) { if (m_owner_mesh_map.find(ownerID) == m_owner_mesh_map.end()) { DEME_ERROR("Owner %zu is not a mesh, you therefore cannot get its nodes' coordinates.", (size_t)ownerID); } + WaitForPendingOutput(); + dT->synchronizeMeshCache(ownerID); float3 mesh_pos = dT->getOwnerPos(ownerID)[0]; float4 mesh_oriQ = dT->getOwnerOriQ(ownerID)[0]; std::vector nodes(m_meshes.at(m_owner_mesh_map.at(ownerID))->GetCoordsVertices()); @@ -1379,6 +1447,8 @@ DEMVisualizationScene DEMSolver::GetVisualizationScene() const { DEME_ERROR("GetVisualizationScene requires Initialize() first."); } ScopedCudaDevice device_scope(dT->streamInfo.device); + WaitForPendingOutput(); + dT->synchronizeMeshCaches(); DEMVisualizationScene scene; scene.revision = dT->visualizationRevision; scene.spheres.reserve(dT->simParams->nSpheresGM); @@ -1435,10 +1505,13 @@ DEMVisualizationSnapshot DEMSolver::GetVisualizationSnapshot(bool include_sphere DEME_ERROR("DEMSolver's method GetVisualizationSnapshot can only be called after calling Initialize()"); } + WaitForPendingOutput(); ScopedCudaDevice device_scope(dT->streamInfo.device); dT->migrateFamilyToHost(); dT->migrateClumpPosInfoToHost(); + if (include_triangles) + dT->synchronizeMeshCaches(); DEMVisualizationSnapshot snapshot; snapshot.simulation_time = GetSimTime(); @@ -3381,12 +3454,14 @@ void DEMSolver::WriteContactFile(const std::string& outfilename, float force_thr } switch (m_cnt_out_format) { case (OUTPUT_FORMAT::CSV): { - dT->migrateFamilyToHost(); - dT->migrateClumpPosInfoToHost(); - dT->migrateContactInfoToHost(); - m_output_thread = std::thread([this, outfilename, force_thres]() { + // Finish reading live solver state before returning to the caller, which may immediately resume dynamics. + // Only the owned snapshot and its column configuration are read by the background writer. + std::shared_ptr contact_info = dT->generateContactInfo(force_thres); + const auto output_flags = dT->solverFlags.cntOutFlags; + const auto wildcard_names = dT->m_contact_wildcard_names; + m_output_thread = std::thread([outfilename, contact_info, output_flags, wildcard_names]() { std::ofstream ptFile(outfilename, std::ios::out); - dT->writeContactsAsCsvFromHost(ptFile, force_thres); + writeContactSnapshotAsCsv(ptFile, *contact_info, output_flags, wildcard_names); }); break; } @@ -3394,12 +3469,14 @@ void DEMSolver::WriteContactFile(const std::string& outfilename, float force_thr // std::ofstream ptFile(outfilename, std::ios::out | std::ios::binary); //// TODO: Implement it DEME_WARNING(std::string("Binary contact pair output is not implemented yet, using CSV...")); - dT->migrateFamilyToHost(); - dT->migrateClumpPosInfoToHost(); - dT->migrateContactInfoToHost(); - m_output_thread = std::thread([this, outfilename, force_thres]() { + // Finish reading live solver state before returning to the caller, which may immediately resume dynamics. + // Only the owned snapshot and its column configuration are read by the background writer. + std::shared_ptr contact_info = dT->generateContactInfo(force_thres); + const auto output_flags = dT->solverFlags.cntOutFlags; + const auto wildcard_names = dT->m_contact_wildcard_names; + m_output_thread = std::thread([outfilename, contact_info, output_flags, wildcard_names]() { std::ofstream ptFile(outfilename, std::ios::out); - dT->writeContactsAsCsvFromHost(ptFile, force_thres); + writeContactSnapshotAsCsv(ptFile, *contact_info, output_flags, wildcard_names); }); break; } @@ -3413,6 +3490,7 @@ void DEMSolver::WriteContactFile(const std::string& outfilename, float force_thr void DEMSolver::WriteMeshFile(const std::string& outfilename) const { ScopedCudaDevice device_scope(dT->streamInfo.device); WaitForPendingOutput(); + dT->synchronizeMeshCaches(); switch (m_mesh_out_format) { case (MESH_FORMAT::VTK): { dT->migrateFamilyToHost(); @@ -3894,7 +3972,7 @@ bool DEMSolver::applyMeshWearModel(bodyID_t ownerID, MeshWearModelState& model) if (mesh_it == m_owner_mesh_map.end()) { DEME_ERROR("Wear model owner %zu is not a mesh owner.", (size_t)ownerID); } - auto& mesh = m_meshes.at(mesh_it->second); + auto& mesh = GetCachedMesh(ownerID); const auto& faces = mesh->GetIndicesVertexes(); const size_t n_tri = faces.size(); if (n_tri != model.tri_count) { @@ -4329,6 +4407,7 @@ void DEMSolver::refreshCombinedRuntimeResources() { } void DEMSolver::Update() { + WaitForPendingOutput(); ++dT->visualizationRevision; if (!sys_initialized) { DEME_ERROR(std::string( diff --git a/src/DEM/AuxClasses.cpp b/src/DEM/AuxClasses.cpp index b9f6cfc0..6d556612 100644 --- a/src/DEM/AuxClasses.cpp +++ b/src/DEM/AuxClasses.cpp @@ -848,17 +848,45 @@ void DEMTracker::SetFamily(unsigned int fam_num, size_t offset) { sys->SetOwnerFamily(obj->ownerID + offset, fam_num); } -void DEMTracker::UpdateMesh(const std::vector& new_nodes) { +void DEMTracker::UpdateMesh(const std::vector& new_nodes, bool update_patch_centers) { assertMesh("UpdateMesh"); // Outsource to API system to handle... - sys->SetTriNodeRelPos(obj->ownerID, obj->geoID, new_nodes); + sys->SetTriNodeRelPos(obj->ownerID, obj->geoID, new_nodes, update_patch_centers); } // Deformation is per-node, yet UpdateTriNodeRelPos need per-triangle info. -void DEMTracker::UpdateMeshByIncrement(const std::vector& deformation) { +void DEMTracker::UpdateMeshByIncrement(const std::vector& deformation, bool update_patch_centers) { assertMesh("UpdateMeshByIncrement"); // Outsource to API system to handle... - sys->UpdateTriNodeRelPos(obj->ownerID, obj->geoID, deformation); + sys->UpdateTriNodeRelPos(obj->ownerID, obj->geoID, deformation, update_patch_centers); +} + +// Tracker forwarding keeps mesh validation and device/cache ownership in the solver. +void DEMTracker::UpdateMeshFromDevice(const float3* source, + int source_device, + bool validate, + bool update_patch_centers) { + assertMesh("UpdateMeshFromDevice"); + sys->SetTriNodeRelPosFromDevice(obj->ownerID, source, source_device, validate, update_patch_centers); +} +void DEMTracker::UpdateMeshByIncrementFromDevice(const float3* source, + int source_device, + bool validate, + bool update_patch_centers) { + assertMesh("UpdateMeshByIncrementFromDevice"); + sys->UpdateTriNodeRelPosFromDevice(obj->ownerID, source, source_device, validate, update_patch_centers); +} +void DEMTracker::UpdateMeshPatchLocations(const std::vector& centers) { + assertMesh("UpdateMeshPatchLocations"); + sys->SetMeshPatchLocations(obj->ownerID, centers); +} +void DEMTracker::UpdateMeshPatchLocationsFromDevice(const float3* source, int source_device, bool validate) { + assertMesh("UpdateMeshPatchLocationsFromDevice"); + sys->SetMeshPatchLocationsFromDevice(obj->ownerID, source, source_device, validate); +} +void DEMTracker::UseAutomaticMeshPatchLocations() { + assertMesh("UseAutomaticMeshPatchLocations"); + sys->UseAutomaticMeshPatchLocations(obj->ownerID); } std::shared_ptr& DEMTracker::GetMesh() { diff --git a/src/DEM/AuxClasses.h b/src/DEM/AuxClasses.h index 6809a1c0..10ada198 100644 --- a/src/DEM/AuxClasses.h +++ b/src/DEM/AuxClasses.h @@ -392,7 +392,10 @@ class DEMTracker { /// deformation), then you should then use tracker to further update the mesh's CoM. /// @param new_nodes New locations of mesh nodes. The length of the argument vector must agree with the number of /// nodes in the tracked mesh. - void UpdateMesh(const std::vector& new_nodes); + /// @param update_patch_centers Refresh automatic centers from all accumulated deformation (default true). + /// False preserves the current centers. This option has no effect for user-supplied centers, which are + /// never overwritten. Automatic single-patch centers remain at the local origin. + void UpdateMesh(const std::vector& new_nodes, bool update_patch_centers = true); /// @brief Change the coordinates of each mesh node by the given amount. /// @details This affects triangle facets' relative positions wrt the mesh center (CoM) only; mesh's overall @@ -402,7 +405,36 @@ class DEMTracker { /// use tracker to further update the mesh's CoM. /// @param deformation Deformation of mesh nodes. The length of the argument vector must agree with the number of /// nodes in the tracked mesh. - void UpdateMeshByIncrement(const std::vector& deformation); + /// @param update_patch_centers Refresh automatic centers from all accumulated deformation (default true). + /// False preserves the current centers. This option has no effect for user-supplied centers, which are + /// never overwritten. Automatic single-patch centers remain at the local origin. + void UpdateMeshByIncrement(const std::vector& deformation, bool update_patch_centers = true); + + /// @brief Replace local mesh node coordinates from a CUDA float3 array in original node order. + /// @details Topology and owner pose are unchanged. The call is synchronous; source may be reused on return. + /// Finish producing source on its CUDA stream before calling. Remote-device input is copied to dT. + /// CPU geometry is synchronized by GetMesh/GetMeshNodesGlobal and solver output/visualization. A previously + /// retained raw mesh handle can remain stale until one of these readers is called. + /// @param validate Check pointer metadata and finite coordinates/results; disable only for trusted input. + /// @param update_patch_centers Recompute automatic centers from the full current geometry. False skips only + /// this call; a later true call includes accumulated deformation. This option has NO effect for user-supplied + /// centers, which are never overwritten. Automatic single-patch centers remain at the local origin; multiple + /// patches use the unweighted mean of triangle centroids. This does not change the contact-rejection heuristic. + void UpdateMeshFromDevice(const float3* source, + int source_device, + bool validate = true, + bool update_patch_centers = true); + /// @brief Add local node displacements from device memory. All other semantics match UpdateMeshFromDevice. + void UpdateMeshByIncrementFromDevice(const float3* source, + int source_device, + bool validate = true, + bool update_patch_centers = true); + /// Set local patch centers at runtime (one per patch), selecting user-managed centers. + void UpdateMeshPatchLocations(const std::vector& centers); + /// Device counterpart: one CUDA float3 per patch. Synchronous; validate checks pointer and finite values. + void UpdateMeshPatchLocationsFromDevice(const float3* source, int source_device, bool validate = true); + /// Resume automatic center updates and immediately recompute from all current geometry. + void UseAutomaticMeshPatchLocations(); /// @brief Get a handle for the mesh this tracker is tracking. /// @return Pointer to the mesh. diff --git a/src/DEM/BdrsAndObjs.h b/src/DEM/BdrsAndObjs.h index e9afce8c..1c44b308 100644 --- a/src/DEM/BdrsAndObjs.h +++ b/src/DEM/BdrsAndObjs.h @@ -743,7 +743,8 @@ class DEMMesh : public DEMInitializer { /// default (single patch). bool ArePatchesExplicitlySet() const { return patches_explicitly_set; } - /// @brief Set the relative location (to CoM) of each patch. + /// @brief Set the relative location (to CoM) of each patch before initialization. + /// For an initialized mesh use tracker UpdateMeshPatchLocations instead; this setter only changes cached data. /// @details Allows user to manually specify the location of each patch relative to the mesh's center of mass. /// @param patch_locations Vector of locations (float3), one for each patch. Must have the same length as the number /// of patches in the mesh. diff --git a/src/DEM/CMakeLists.txt b/src/DEM/CMakeLists.txt index 4ca74fd8..a4cb9ac8 100644 --- a/src/DEM/CMakeLists.txt +++ b/src/DEM/CMakeLists.txt @@ -40,6 +40,7 @@ set(DEM_headers ${CMAKE_CURRENT_SOURCE_DIR}/utils/CombinedOwnerUtils.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/HostSideHelpers.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/MeshUtils.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/MeshDeformation.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/Samplers.hpp ${CMAKE_CURRENT_SOURCE_DIR}/AuxClasses.h ) diff --git a/src/DEM/dT.cpp b/src/DEM/dT.cpp index 2154c82f..5df0fe9f 100644 --- a/src/DEM/dT.cpp +++ b/src/DEM/dT.cpp @@ -12,6 +12,8 @@ #include #include #include +#include "utils/ContactOutput.hpp" +#include "algorithms/DEMMeshDeformation.h" #include #include #include @@ -236,6 +238,7 @@ void DEMDynamicThread::migrateDataToDevice() { } void DEMDynamicThread::migrateDeviceModifiableInfoToHost() { + synchronizeMeshCaches(); migrateClumpPosInfoToHost(); migrateClumpHighOrderInfoToHost(); migrateFamilyToHost(); @@ -1122,6 +1125,7 @@ void DEMDynamicThread::populateEntityArrays(const std::vectorClearWildcards(); @@ -1912,99 +1918,12 @@ void DEMDynamicThread::writeContactsAsCsv(std::ofstream& ptFile, float force_thr } void DEMDynamicThread::writeContactsAsCsvFromHost(std::ofstream& ptFile, float force_thres) { - std::ostringstream outstrstream; - - std::shared_ptr contactInfo = generateContactInfoFromHost(force_thres); - - outstrstream << OUTPUT_FILE_CNT_TYPE_NAME; - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::OWNER) { - outstrstream << "," + OUTPUT_FILE_OWNER_1_NAME + "," + OUTPUT_FILE_OWNER_2_NAME; - } - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::GEO_ID) { - outstrstream << "," + OUTPUT_FILE_GEO_ID_1_NAME + "," + OUTPUT_FILE_GEO_ID_2_NAME; - } - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::FORCE) { - outstrstream << "," + OUTPUT_FILE_FORCE_X_NAME + "," + OUTPUT_FILE_FORCE_Y_NAME + "," + - OUTPUT_FILE_FORCE_Z_NAME; - } - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::CNT_POINT) { - outstrstream << "," + OUTPUT_FILE_X_COL_NAME + "," + OUTPUT_FILE_Y_COL_NAME + "," + OUTPUT_FILE_Z_COL_NAME; - } - // if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::COMPONENT) { - // outstrstream << ","+OUTPUT_FILE_COMP_1_NAME+","+OUTPUT_FILE_COMP_2_NAME; - // } - // if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::NICKNAME) { - // outstrstream << ","+OUTPUT_FILE_OWNER_NICKNAME_1_NAME+","+OUTPUT_FILE_OWNER_NICKNAME_2_NAME; - // } - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::NORMAL) { - outstrstream << "," + OUTPUT_FILE_NORMAL_X_NAME + "," + OUTPUT_FILE_NORMAL_Y_NAME + "," + - OUTPUT_FILE_NORMAL_Z_NAME; - } - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::TORQUE) { - outstrstream << "," + OUTPUT_FILE_TORQUE_X_NAME + "," + OUTPUT_FILE_TORQUE_Y_NAME + "," + - OUTPUT_FILE_TORQUE_Z_NAME; - } - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::CNT_WILDCARD) { - // Write all wildcard names as header - for (const auto& w_name : m_contact_wildcard_names) { - outstrstream << "," + w_name; - } - } - outstrstream << "\n"; - - for (size_t i = 0; i < contactInfo->Size(); i++) { - outstrstream << contactInfo->Get("ContactType")[i]; - - // (Internal) ownerID and/or geometry ID - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::OWNER) { - outstrstream << "," << contactInfo->Get("AOwner")[i] << "," - << contactInfo->Get("BOwner")[i]; - } - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::GEO_ID) { - outstrstream << "," << contactInfo->Get("AGeo")[i] << "," - << contactInfo->Get("BGeo")[i]; - } - - // Force is already in global... - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::FORCE) { - outstrstream << "," << contactInfo->Get("Force")[i].x << "," - << contactInfo->Get("Force")[i].y << "," << contactInfo->Get("Force")[i].z; - } - - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::CNT_POINT) { - // oriQ is updated already... whereas the contact point is effectively last step's... That's unfortunate. - // Should we do somthing ahout it? - outstrstream << "," << contactInfo->Get("Point")[i].x << "," - << contactInfo->Get("Point")[i].y << "," << contactInfo->Get("Point")[i].z; - } - - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::NORMAL) { - outstrstream << "," << contactInfo->Get("Normal")[i].x << "," - << contactInfo->Get("Normal")[i].y << "," << contactInfo->Get("Normal")[i].z; - } - - // Torque is in global already... - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::TORQUE) { - outstrstream << "," << contactInfo->Get("Torque")[i].x << "," - << contactInfo->Get("Torque")[i].y << "," << contactInfo->Get("Torque")[i].z; - } - - // Contact wildcards - if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::CNT_WILDCARD) { - // The order shouldn't be an issue... the same set is being processed here and in equip_contact_wildcards, - // see Model.h - for (const auto& name : m_contact_wildcard_names) { - outstrstream << "," << contactInfo->Get(name)[i]; - } - } - - outstrstream << "\n"; - } - - ptFile << outstrstream.str(); + const auto contact_info = generateContactInfoFromHost(force_thres); + writeContactSnapshotAsCsv(ptFile, *contact_info, solverFlags.cntOutFlags, m_contact_wildcard_names); } void DEMDynamicThread::writeMeshesAsVtk(std::ofstream& ptFile) { + synchronizeMeshCaches(); migrateFamilyToHost(); migrateClumpPosInfoToHost(); writeMeshesAsVtkFromHost(ptFile); @@ -2233,6 +2152,7 @@ void DEMDynamicThread::writeMeshesAsVtkFromHost(std::ofstream& ptFile) { } void DEMDynamicThread::writeMeshesAsStl(std::ofstream& ptFile) { + synchronizeMeshCaches(); migrateFamilyToHost(); migrateClumpPosInfoToHost(); writeMeshesAsStlFromHost(ptFile); @@ -2302,6 +2222,7 @@ void DEMDynamicThread::writeMeshesAsStlFromHost(std::ofstream& ptFile) { } void DEMDynamicThread::writeMeshesAsPly(std::ofstream& ptFile, bool patch_colors) { + synchronizeMeshCaches(); migrateFamilyToHost(); migrateClumpPosInfoToHost(); writeMeshesAsPlyFromHost(ptFile, patch_colors); @@ -4689,6 +4610,155 @@ void DEMDynamicThread::setOwnerFamily(bodyID_t ownerID, family_t fam, bodyID_t n familyID.setVal(std::vector(n, fam), ownerID); } +// Build persistent connectivity and patch reductions once, after initialization has corrected triangle winding. +void DEMDynamicThread::initializeMeshDeformation(const std::shared_ptr& mesh, + size_t tri_start, + size_t patch_start) { + auto state = std::make_unique(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + state->mesh = mesh; + state->tri_start = tri_start; + state->patch_start = patch_start; + state->vertices.resizeHost(mesh->GetNumNodes()); + std::copy(mesh->m_vertices.begin(), mesh->m_vertices.end(), state->vertices.host()); + state->input.resizeHost(std::max(mesh->GetNumNodes(), (size_t)mesh->GetNumPatches())); + state->faces.resizeHost(mesh->GetNumTriangles()); + std::copy(mesh->m_face_v_indices.begin(), mesh->m_face_v_indices.end(), state->faces.host()); + // Initialization may reverse the triangle soup to honor supplied normals without changing mesh face indices. + for (size_t t = 0; t < state->faces.size(); ++t) { + if (mesh->use_mesh_normals) { + const auto tri = mesh->GetTriangle(t); + const float3 normal = mesh->m_normals.at(mesh->m_face_n_indices.at(t).x); + if (dot(cross(tri.p2 - tri.p1, tri.p3 - tri.p1), normal) < 0) { + std::swap(state->faces[t].y, state->faces[t].z); + } + } + } + const size_t patches = mesh->GetNumPatches(); + state->patch_offsets.resizeHost(patches + 1, 0); + state->patch_triangles.resizeHost(mesh->GetNumTriangles()); + for (size_t t = 0; t < mesh->GetNumTriangles(); ++t) { + const size_t p = triPatchID[tri_start + t] - patch_start; + if (p >= patches) + DEME_ERROR("Invalid patch index in mesh deformation connectivity."); + ++state->patch_offsets[p + 1]; + } + for (size_t p = 0; p < patches; ++p) + state->patch_offsets[p + 1] += state->patch_offsets[p]; + std::vector cursor(state->patch_offsets.host(), state->patch_offsets.host() + patches); + for (size_t t = 0; t < mesh->GetNumTriangles(); ++t) { + const size_t p = triPatchID[tri_start + t] - patch_start; + state->patch_triangles[cursor[p]++] = t; + } + state->invalid.resizeHost(1, 0u); + mesh->m_patch_locations.assign(relPosPatch.host() + patch_start, relPosPatch.host() + patch_start + patches); + meshDeformation[mesh->owner] = std::move(state); +} + +// Recompute from all current triangles, including deformation accumulated while refresh was disabled. +void DEMDynamicThread::refreshMeshPatchCenters(bodyID_t owner) { + auto& state = *meshDeformation.at(owner); + state.prepareDevice(); + if (state.mesh->patch_locations_explicitly_set) + return; + RefreshMeshPatchCenters(state.patch_offsets.data(), state.patch_triangles.data(), state.patch_offsets.size() - 1, + relPosNode1.data() + state.tri_start, relPosNode2.data() + state.tri_start, + relPosNode3.data() + state.tri_start, relPosPatch.data() + state.patch_start, + streamInfo.stream); + state.centers_dirty = true; +} + +// Same-device input is consumed directly; remote input uses the persistent staging buffer. Calls are synchronous +// so the caller can reuse its input immediately, without requiring a stream interoperability contract. +void DEMDynamicThread::deformMesh(bodyID_t owner, + const float3* source, + int source_device, + bool increment, + bool validate, + bool update_patch_centers) { + auto& state = *meshDeformation.at(owner); + state.prepareDevice(); + const size_t count = state.vertices.size(); + if (validate) + DEME_GPU_CALL(device_data::ValidateOutputPointer(source, count * sizeof(float3), source_device)); + if (source_device != streamInfo.device) { + DEME_GPU_CALL(ownerDataTransferBuffer.Copy(state.input.data(), streamInfo.device, source, source_device, + count * sizeof(float3))); + source = state.input.data(); + } + if (validate) { + DEME_GPU_CALL(cudaMemsetAsync(state.invalid.data(), 0, sizeof(unsigned int), streamInfo.stream)); + ValidateMeshVectors(source, increment ? state.vertices.data() : nullptr, count, state.invalid.data(), + streamInfo.stream); + state.invalid.toHostAsync(streamInfo.stream); + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + if (state.invalid[0]) + DEME_ERROR("Mesh deformation requires finite node coordinates and results."); + } + DeformMeshVertices(source, state.vertices.data(), count, increment, state.faces.data(), state.faces.size(), + relPosNode1.data() + state.tri_start, relPosNode2.data() + state.tri_start, + relPosNode3.data() + state.tri_start, streamInfo.stream); + if (update_patch_centers) + refreshMeshPatchCenters(owner); + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + state.vertices_dirty = true; + solverFlags.willMeshDeform = true; + ++visualizationRevision; +} + +// Explicit runtime centers update dT storage as well as the mesh cache; subsequent deformations preserve them. +void DEMDynamicThread::setMeshPatchLocations(bodyID_t owner, const float3* source, int source_device, bool validate) { + auto& state = *meshDeformation.at(owner); + state.prepareDevice(); + const size_t count = state.patch_offsets.size() - 1; + if (validate) + DEME_GPU_CALL(device_data::ValidateOutputPointer(source, count * sizeof(float3), source_device)); + if (source_device != streamInfo.device) { + DEME_GPU_CALL(ownerDataTransferBuffer.Copy(state.input.data(), streamInfo.device, source, source_device, + count * sizeof(float3))); + source = state.input.data(); + } + if (validate) { + DEME_GPU_CALL(cudaMemsetAsync(state.invalid.data(), 0, sizeof(unsigned int), streamInfo.stream)); + ValidateMeshVectors(source, nullptr, count, state.invalid.data(), streamInfo.stream); + state.invalid.toHostAsync(streamInfo.stream); + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + if (state.invalid[0]) + DEME_ERROR("Mesh patch locations must be finite."); + } + DEME_GPU_CALL(cudaMemcpyAsync(relPosPatch.data() + state.patch_start, source, count * sizeof(float3), + cudaMemcpyDeviceToDevice, streamInfo.stream)); + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + state.mesh->patch_locations_explicitly_set = true; + state.centers_dirty = true; +} + +// Download only at CPU observation boundaries. Previously retained raw mesh handles are snapshots until a tracker +// getter or another solver CPU reader synchronizes them; direct writes to mesh storage are not runtime updates. +void DEMDynamicThread::synchronizeMeshCache(bodyID_t owner) { + auto& state = *meshDeformation.at(owner); + if (state.vertices_dirty) { + state.vertices.toHost(); + state.mesh->m_vertices.assign(state.vertices.host(), state.vertices.host() + state.vertices.size()); + relPosNode1.toHost(state.tri_start, state.faces.size()); + relPosNode2.toHost(state.tri_start, state.faces.size()); + relPosNode3.toHost(state.tri_start, state.faces.size()); + state.vertices_dirty = false; + } + if (state.centers_dirty) { + const size_t count = state.patch_offsets.size() - 1; + relPosPatch.toHost(state.patch_start, count); + state.mesh->m_patch_locations.assign(relPosPatch.host() + state.patch_start, + relPosPatch.host() + state.patch_start + count); + state.centers_dirty = false; + } +} + +// Used before output, visualization, and any full host-to-device state reload. +void DEMDynamicThread::synchronizeMeshCaches() { + for (const auto& entry : meshDeformation) + synchronizeMeshCache(entry.first); +} + void DEMDynamicThread::setTriNodeRelPos(size_t start, const std::vector& triangles) { ++visualizationRevision; for (size_t i = 0; i < triangles.size(); i++) { diff --git a/src/DEM/dT.h b/src/DEM/dT.h index a8030546..6d7420af 100644 --- a/src/DEM/dT.h +++ b/src/DEM/dT.h @@ -25,6 +25,7 @@ #include "Structs.h" #include "AuxClasses.h" #include "utils/DynamicThreadHelpers.hpp" +#include "utils/MeshDeformation.hpp" namespace deme { @@ -74,6 +75,7 @@ class DEMDynamicThread { DEMSolverScratchData solverScratchSpace = DEMSolverScratchData(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); // Reuses pinned staging storage only when a requested destination GPU cannot access dT's GPU directly. device_data::TransferBuffer ownerDataTransferBuffer; + std::unordered_map> meshDeformation; // The number of for iterations dT does for a specific user "run simulation" call double cycleDuration; @@ -510,6 +512,18 @@ class DEMDynamicThread { /// @brief Add an extra angular acceleration to consecutive owners for the next time step. void addOwnerNextStepAngAcc(bodyID_t ownerID, const std::vector& angAcc); + // Fixed-topology deformation and lazy CPU cache synchronization, shared by host/device APIs. + void initializeMeshDeformation(const std::shared_ptr& mesh, size_t tri_start, size_t patch_start); + void deformMesh(bodyID_t owner, + const float3* source, + int source_device, + bool increment, + bool validate, + bool update_patch_centers); + void refreshMeshPatchCenters(bodyID_t owner); + void setMeshPatchLocations(bodyID_t owner, const float3* source, int source_device, bool validate); + void synchronizeMeshCache(bodyID_t owner); + void synchronizeMeshCaches(); /// Rewrite the relative positions of the flattened triangle soup, starting from `start', using triangle nodal /// positions in `triangles'. void setTriNodeRelPos(size_t start, const std::vector& triangles); diff --git a/src/DEM/kT.cpp b/src/DEM/kT.cpp index 80fc0043..9b4428de 100644 --- a/src/DEM/kT.cpp +++ b/src/DEM/kT.cpp @@ -509,6 +509,7 @@ void DEMKinematicThread::packDataPointers() { ownerTriMesh.bindDevicePointer(&(granData->ownerTriMesh)); ownerMeshConvex.bindDevicePointer(&(granData->ownerMeshConvex)); ownerMeshNeverWinner.bindDevicePointer(&(granData->ownerMeshNeverWinner)); + ownerMeshShellHalfThickness.bindDevicePointer(&(granData->ownerMeshShellHalfThickness)); triPatchID.bindDevicePointer(&(granData->triPatchID)); triNeighborIndex.bindDevicePointer(&(granData->triNeighborIndex)); triNeighbor1.bindDevicePointer(&(granData->triNeighbor1)); @@ -564,6 +565,7 @@ void DEMKinematicThread::migrateDataToDevice() { ownerTriMesh.toDeviceAsync(streamInfo.stream); ownerMeshConvex.toDeviceAsync(streamInfo.stream); ownerMeshNeverWinner.toDeviceAsync(streamInfo.stream); + ownerMeshShellHalfThickness.toDeviceAsync(streamInfo.stream); triPatchID.toDeviceAsync(streamInfo.stream); triNeighborIndex.toDeviceAsync(streamInfo.stream); triNeighbor1.toDeviceAsync(streamInfo.stream); @@ -710,6 +712,7 @@ void DEMKinematicThread::allocateGPUArrays(size_t nOwnerBodies, DEME_DUAL_ARRAY_RESIZE(oriQz, nOwnerBodies, 0); DEME_DUAL_ARRAY_RESIZE(ownerMeshConvex, nOwnerBodies, 0); DEME_DUAL_ARRAY_RESIZE(ownerMeshNeverWinner, nOwnerBodies, 0); + DEME_DUAL_ARRAY_RESIZE(ownerMeshShellHalfThickness, nOwnerBodies, 0.f); DEME_DEVICE_ARRAY_RESIZE(marginSizeSphere, nSpheresGM); DEME_DEVICE_ARRAY_RESIZE(marginSizeAnalytical, nAnalGM); DEME_DEVICE_ARRAY_RESIZE(marginSizeTriangle, nTriGM); @@ -828,6 +831,7 @@ void DEMKinematicThread::registerPolicies(const std::vector& fa void DEMKinematicThread::populateEntityArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_family, const std::vector& input_mesh_obj_convex, const std::vector& input_mesh_obj_never_winner, @@ -978,6 +982,8 @@ void DEMKinematicThread::populateEntityArrays(const std::vectorGetShellHalfThickness(), 0.f); // DEME_DEBUG_PRINTF("kT just loaded a mesh in family %u", +(this_family_num)); // DEME_DEBUG_PRINTF("Number of triangle facets loaded thus far: %zu", k); } @@ -985,6 +991,7 @@ void DEMKinematicThread::populateEntityArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_family, const std::vector& input_mesh_obj_convex, const std::vector& input_mesh_obj_never_winner, @@ -1002,14 +1009,16 @@ void DEMKinematicThread::initGPUArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_family, const std::vector& input_mesh_obj_convex, const std::vector& input_mesh_obj_never_winner, @@ -1031,11 +1040,11 @@ void DEMKinematicThread::updateClumpMeshArrays(const std::vector& dT_data, size_t nContacts) { diff --git a/src/DEM/kT.h b/src/DEM/kT.h index 454b2164..b9f413ca 100644 --- a/src/DEM/kT.h +++ b/src/DEM/kT.h @@ -65,7 +65,8 @@ class DEMKinematicThread { DualStruct simParams = DualStruct(); // Pointers to those data arrays defined below, stored in a struct - DualStruct granData = DualStruct(); + // DualStruct's default constructor allocates raw storage without applying DEMDataKT's member initializers. + DualStruct granData = DualStruct(DEMDataKT{}); // Buffer arrays for storing info from the dT side. // dT modifies these arrays; kT uses them only. @@ -208,6 +209,9 @@ class DEMKinematicThread { DualArray ownerMeshNeverWinner = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Per-owner shell extent used when constructing the contact-detection triangle prisms. + DualArray ownerMeshShellHalfThickness = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Mesh patch information: each facet belongs to a patch // Patch ID for each triangle facet (maps facet to patch) DualArray triPatchID = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); @@ -339,6 +343,7 @@ class DEMKinematicThread { void registerPolicies(const std::vector& family_mask_matrix); void populateEntityArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_family, const std::vector& input_mesh_obj_convex, const std::vector& input_mesh_obj_never_winner, @@ -359,6 +364,7 @@ class DEMKinematicThread { /// Initialize arrays void initGPUArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_family, const std::vector& input_mesh_obj_convex, const std::vector& input_mesh_obj_never_winner, @@ -376,6 +382,7 @@ class DEMKinematicThread { /// no other changes to the system. void updateClumpMeshArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_family, const std::vector& input_mesh_obj_convex, const std::vector& input_mesh_obj_never_winner, diff --git a/src/DEM/python/bindings.cpp b/src/DEM/python/bindings.cpp index 03e703b0..5b2fe887 100644 --- a/src/DEM/python/bindings.cpp +++ b/src/DEM/python/bindings.cpp @@ -172,7 +172,7 @@ PYBIND11_MODULE(_deme, obj) { py::arg("spacing") = 1.2, "Return points on a cylindrical surface. ``spacing`` scales the nominal particle-diameter separation."); - obj.attr("PI") = py::float_(M_PI); + obj.attr("PI") = py::float_(deme::PI); // Export both the precise name and its legacy mesh-oriented spelling. They deliberately carry the same value. obj.attr("SPHERE_TRIANGLE_CONTACT") = py::int_(static_cast(deme::SPHERE_TRIANGLE_CONTACT)); obj.attr("SPHERE_MESH_CONTACT") = py::int_(static_cast(deme::SPHERE_MESH_CONTACT)); @@ -691,14 +691,57 @@ when this call returns.)doc", .def("SetFamily", static_cast(&deme::DEMTracker::SetFamily), "Change the family number of one entities tracked by this tracker.", py::arg("fam_num"), py::arg("offset")) - .def("UpdateMesh", - static_cast&)>(&deme::DEMTracker::UpdateMesh), - "Apply the new mesh node positions such that the tracked mesh is replaced by the new_nodes.", - py::arg("new_nodes")) - .def("UpdateMeshByIncrement", - static_cast&)>( - &deme::DEMTracker::UpdateMeshByIncrement), - "Change the coordinates of each mesh node by the given amount.", py::arg("deformation")) + .def("UpdateMesh", &deme::DEMTracker::UpdateMesh, + "Replace local mesh node coordinates. update_patch_centers refreshes automatic centers from all " + "accumulated deformation; false preserves current centers. It has no effect for user-supplied centers. " + "Automatic single-patch centers remain at the local origin.", + py::arg("new_nodes"), py::arg("update_patch_centers") = true) + .def("UpdateMeshByIncrement", &deme::DEMTracker::UpdateMeshByIncrement, + "Add local mesh node displacements. update_patch_centers refreshes automatic centers from all " + "accumulated deformation; false preserves current centers. It has no effect for user-supplied centers. " + "Automatic single-patch centers remain at the local origin.", + py::arg("deformation"), py::arg("update_patch_centers") = true) + + .def( + "UpdateMeshFromDevice", + [](deme::DEMTracker& self, std::uintptr_t source, int source_device, bool validate, + bool update_patch_centers) { + self.UpdateMeshFromDevice(reinterpret_cast(source), source_device, validate, + update_patch_centers); + }, + "Synchronous local-node device update: one float3 per node, fixed topology. Finish producing source " + "before calling. validate checks pointer and finite values. update_patch_centers refreshes automatic " + "centers only and has no effect for user-supplied centers. Single-patch automatic centers stay at the " + "local origin. Tracker GetMesh/GetMeshNodesGlobal synchronize the lazy CPU geometry cache.", + py::arg("source"), py::arg("source_device"), py::arg("validate") = true, + py::arg("update_patch_centers") = true) + .def( + "UpdateMeshByIncrementFromDevice", + [](deme::DEMTracker& self, std::uintptr_t source, int source_device, bool validate, + bool update_patch_centers) { + self.UpdateMeshByIncrementFromDevice(reinterpret_cast(source), source_device, validate, + update_patch_centers); + }, + "Synchronous local-node displacement increment: one float3 per node, fixed topology. Finish producing " + "source " + "before calling. validate checks pointer and finite values. update_patch_centers refreshes automatic " + "centers only and has no effect for user-supplied centers. Single-patch automatic centers stay at the " + "local origin. A later enabled refresh includes skipped deformation. Tracker GetMesh/GetMeshNodesGlobal " + "synchronize the lazy CPU geometry cache.", + py::arg("source"), py::arg("source_device"), py::arg("validate") = true, + py::arg("update_patch_centers") = true) + .def("UpdateMeshPatchLocations", &deme::DEMTracker::UpdateMeshPatchLocations, + "Set one local center per patch at runtime and select user-managed centers.", py::arg("centers")) + .def( + "UpdateMeshPatchLocationsFromDevice", + [](deme::DEMTracker& self, std::uintptr_t source, int source_device, bool validate) { + self.UpdateMeshPatchLocationsFromDevice(reinterpret_cast(source), source_device, + validate); + }, + "Synchronously set user-managed local patch centers from one CUDA float3 per patch.", py::arg("source"), + py::arg("source_device"), py::arg("validate") = true) + .def("UseAutomaticMeshPatchLocations", &deme::DEMTracker::UseAutomaticMeshPatchLocations, + "Resume automatic centers and immediately recompute from current geometry.") .def("GetMeshNodesGlobal", static_cast (deme::DEMTracker::*)()>(&deme::DEMTracker::GetMeshNodesGlobal), @@ -1643,10 +1686,50 @@ enabled. A zero count is a no-op.)doc", .def("SetOwnerFamily", &deme::DEMSolver::SetOwnerFamily, "Set the family number of consecutive owners.", py::arg("ownerID"), py::arg("fam"), py::arg("n") = 1) + .def("SetTriNodeRelPosFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner, std::uintptr_t source, int source_device, bool validate, + bool update_patch_centers) { + self.SetTriNodeRelPosFromDevice(owner, reinterpret_cast(source), source_device, validate, + update_patch_centers); + }, + "Synchronous local-node device update: one float3 per node, fixed topology. Finish producing source " + "before calling. validate checks pointer and finite values. update_patch_centers refreshes automatic " + "centers only and has no effect for user-supplied centers. Single-patch automatic centers stay at the " + "local origin. Tracker GetMesh/GetMeshNodesGlobal synchronize the lazy CPU geometry cache.", + py::arg("owner"), py::arg("source"), py::arg("source_device"), py::arg("validate") = true, + py::arg("update_patch_centers") = true) + .def("UpdateTriNodeRelPosFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner, std::uintptr_t source, int source_device, bool validate, + bool update_patch_centers) { + self.UpdateTriNodeRelPosFromDevice(owner, reinterpret_cast(source), source_device, validate, + update_patch_centers); + }, + "Synchronous local-node displacement increment: one float3 per node, fixed topology. Finish producing source " + "before calling. validate checks pointer and finite values. update_patch_centers refreshes automatic " + "centers only and has no effect for user-supplied centers. Single-patch automatic centers stay at the " + "local origin. A later enabled refresh includes skipped deformation. Tracker GetMesh/GetMeshNodesGlobal synchronize the lazy CPU geometry cache.", + py::arg("owner"), py::arg("source"), py::arg("source_device"), py::arg("validate") = true, + py::arg("update_patch_centers") = true) + .def("SetMeshPatchLocations", &deme::DEMSolver::SetMeshPatchLocations, + "Set one local center per patch at runtime and select user-managed centers.", py::arg("owner"), py::arg("centers")) + .def("SetMeshPatchLocationsFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner, std::uintptr_t source, int source_device, bool validate) { + self.SetMeshPatchLocationsFromDevice(owner, reinterpret_cast(source), source_device, validate); + }, "Synchronously set user-managed local patch centers from one CUDA float3 per patch.", + py::arg("owner"), py::arg("source"), py::arg("source_device"), py::arg("validate") = true) + .def("UseAutomaticMeshPatchLocations", &deme::DEMSolver::UseAutomaticMeshPatchLocations, + "Resume automatic centers and immediately recompute from current geometry.", py::arg("owner")) + .def("SetTriNodeRelPos", &deme::DEMSolver::SetTriNodeRelPos, - "Rewrite the relative positions of the flattened triangle soup.") + "Rewrite local mesh node coordinates. update_patch_centers refreshes automatic centers from all " + "accumulated deformation; false preserves current centers. It has no effect for user-supplied centers. " + "Automatic single-patch centers remain at the local origin.", + py::arg("owner"), py::arg("triID"), py::arg("new_nodes"), py::arg("update_patch_centers") = true) .def("UpdateTriNodeRelPos", &deme::DEMSolver::UpdateTriNodeRelPos, - "Update the relative positions of the flattened triangle soup.") + "Update local mesh node coordinates. update_patch_centers refreshes automatic centers from all " + "accumulated deformation; false preserves current centers. It has no effect for user-supplied centers. " + "Automatic single-patch centers remain at the local origin.", + py::arg("owner"), py::arg("triID"), py::arg("updates"), py::arg("update_patch_centers") = true) .def("GetCachedMesh", &deme::DEMSolver::GetCachedMesh, "Get a handle for the mesh this tracker is tracking.") .def("GetMeshNodesGlobal", &deme::DEMSolver::GetMeshNodesGlobal, "Get the current locations of all the nodes in the mesh being tracked.") @@ -2334,6 +2417,12 @@ enabled. A zero count is a no-op.)doc", "Assign every triangle to its own patch using consecutive patch IDs.") .def("GetPatchIDs", &deme::DEMMeshConnected::GetPatchIDs, "Get one patch ID per triangle.") .def("GetNumPatches", &deme::DEMMeshConnected::GetNumPatches, "Get the number of mesh patches.") + .def("SetPatchLocations", &deme::DEMMeshConnected::SetPatchLocations, + "Set local patch centers before initialization. Use tracker UpdateMeshPatchLocations at runtime.", + py::arg("centers")) + .def("GetPatchLocations", &deme::DEMMeshConnected::GetPatchLocations, + "Read cached patch centers; call tracker GetMesh first after device updates to synchronize.") + .def("ArePatchLocationsExplicitlySet", &deme::DEMMeshConnected::ArePatchLocationsExplicitlySet) .def("ArePatchesExplicitlySet", &deme::DEMMeshConnected::ArePatchesExplicitlySet, "Return whether patch IDs were explicitly supplied or computed.") .def("SplitIntoConvexPatches", diff --git a/src/DEM/utils/AnalyticalOutput.cpp b/src/DEM/utils/AnalyticalOutput.cpp index 4a2b36bf..089389fa 100644 --- a/src/DEM/utils/AnalyticalOutput.cpp +++ b/src/DEM/utils/AnalyticalOutput.cpp @@ -172,8 +172,8 @@ void appendRoundSurface(std::vector& output, float3 basis_u, basis_v; orthogonalBasis(axis, basis_u, basis_v); for (unsigned int i = 0; i < resolution; i++) { - const float angle_0 = 2.f * static_cast(M_PI) * i / resolution; - const float angle_1 = 2.f * static_cast(M_PI) * (i + 1) / resolution; + const float angle_0 = 2.f * static_cast(PI) * i / resolution; + const float angle_1 = 2.f * static_cast(PI) * (i + 1) / resolution; const float3 radial_0 = basis_u * std::cos(angle_0) + basis_v * std::sin(angle_0); const float3 radial_1 = basis_u * std::cos(angle_1) + basis_v * std::sin(angle_1); const float radius_min = is_cylinder ? surface.size_1 : surface.size_1 * axial_min; diff --git a/src/DEM/utils/ContactOutput.hpp b/src/DEM/utils/ContactOutput.hpp new file mode 100644 index 00000000..42cbe0e0 --- /dev/null +++ b/src/DEM/utils/ContactOutput.hpp @@ -0,0 +1,111 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include +#include +#include +#include + +#include "../Structs.h" + +namespace deme { + +// Serialize an owned contact snapshot using the matching column configuration. This helper deliberately has no +// solver access, so asynchronous formatting and disk I/O cannot observe a later frame or resized host arrays. +inline void writeContactSnapshotAsCsv(std::ostream& ptFile, + const ContactInfoContainer& contact_info, + unsigned int output_flags, + const std::set& wildcard_names) { + std::ostringstream outstrstream; + + outstrstream << OUTPUT_FILE_CNT_TYPE_NAME; + if (output_flags & CNT_OUTPUT_CONTENT::OWNER) { + outstrstream << "," + OUTPUT_FILE_OWNER_1_NAME + "," + OUTPUT_FILE_OWNER_2_NAME; + } + if (output_flags & CNT_OUTPUT_CONTENT::GEO_ID) { + outstrstream << "," + OUTPUT_FILE_GEO_ID_1_NAME + "," + OUTPUT_FILE_GEO_ID_2_NAME; + } + if (output_flags & CNT_OUTPUT_CONTENT::FORCE) { + outstrstream << "," + OUTPUT_FILE_FORCE_X_NAME + "," + OUTPUT_FILE_FORCE_Y_NAME + "," + + OUTPUT_FILE_FORCE_Z_NAME; + } + if (output_flags & CNT_OUTPUT_CONTENT::CNT_POINT) { + outstrstream << "," + OUTPUT_FILE_X_COL_NAME + "," + OUTPUT_FILE_Y_COL_NAME + "," + OUTPUT_FILE_Z_COL_NAME; + } + // if (output_flags & CNT_OUTPUT_CONTENT::COMPONENT) { + // outstrstream << ","+OUTPUT_FILE_COMP_1_NAME+","+OUTPUT_FILE_COMP_2_NAME; + // } + // if (output_flags & CNT_OUTPUT_CONTENT::NICKNAME) { + // outstrstream << ","+OUTPUT_FILE_OWNER_NICKNAME_1_NAME+","+OUTPUT_FILE_OWNER_NICKNAME_2_NAME; + // } + if (output_flags & CNT_OUTPUT_CONTENT::NORMAL) { + outstrstream << "," + OUTPUT_FILE_NORMAL_X_NAME + "," + OUTPUT_FILE_NORMAL_Y_NAME + "," + + OUTPUT_FILE_NORMAL_Z_NAME; + } + if (output_flags & CNT_OUTPUT_CONTENT::TORQUE) { + outstrstream << "," + OUTPUT_FILE_TORQUE_X_NAME + "," + OUTPUT_FILE_TORQUE_Y_NAME + "," + + OUTPUT_FILE_TORQUE_Z_NAME; + } + if (output_flags & CNT_OUTPUT_CONTENT::CNT_WILDCARD) { + // Write all wildcard names as header + for (const auto& w_name : wildcard_names) { + outstrstream << "," + w_name; + } + } + outstrstream << "\n"; + + for (size_t i = 0; i < contact_info.Size(); i++) { + outstrstream << contact_info.Get("ContactType")[i]; + + // (Internal) ownerID and/or geometry ID + if (output_flags & CNT_OUTPUT_CONTENT::OWNER) { + outstrstream << "," << contact_info.Get("AOwner")[i] << "," + << contact_info.Get("BOwner")[i]; + } + if (output_flags & CNT_OUTPUT_CONTENT::GEO_ID) { + outstrstream << "," << contact_info.Get("AGeo")[i] << "," + << contact_info.Get("BGeo")[i]; + } + + // Force is already in global... + if (output_flags & CNT_OUTPUT_CONTENT::FORCE) { + outstrstream << "," << contact_info.Get("Force")[i].x << "," + << contact_info.Get("Force")[i].y << "," << contact_info.Get("Force")[i].z; + } + + if (output_flags & CNT_OUTPUT_CONTENT::CNT_POINT) { + // oriQ is updated already... whereas the contact point is effectively last step's... That's unfortunate. + // Should we do somthing ahout it? + outstrstream << "," << contact_info.Get("Point")[i].x << "," + << contact_info.Get("Point")[i].y << "," << contact_info.Get("Point")[i].z; + } + + if (output_flags & CNT_OUTPUT_CONTENT::NORMAL) { + outstrstream << "," << contact_info.Get("Normal")[i].x << "," + << contact_info.Get("Normal")[i].y << "," << contact_info.Get("Normal")[i].z; + } + + // Torque is in global already... + if (output_flags & CNT_OUTPUT_CONTENT::TORQUE) { + outstrstream << "," << contact_info.Get("Torque")[i].x << "," + << contact_info.Get("Torque")[i].y << "," << contact_info.Get("Torque")[i].z; + } + + // Contact wildcards + if (output_flags & CNT_OUTPUT_CONTENT::CNT_WILDCARD) { + // The order shouldn't be an issue... the same set is being processed here and in equip_contact_wildcards, + // see Model.h + for (const auto& name : wildcard_names) { + outstrstream << "," << contact_info.Get(name)[i]; + } + } + + outstrstream << "\n"; + } + + ptFile << outstrstream.str(); +} + +} // namespace deme diff --git a/src/DEM/utils/MeshDeformation.hpp b/src/DEM/utils/MeshDeformation.hpp new file mode 100644 index 00000000..623143a0 --- /dev/null +++ b/src/DEM/utils/MeshDeformation.hpp @@ -0,0 +1,49 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#ifndef DEME_MESH_DEFORMATION_HPP +#define DEME_MESH_DEFORMATION_HPP + +#include + +#include "core/utils/DataMigrationHelper.hpp" +#include "DEM/BdrsAndObjs.h" + +namespace deme { +// Persistent, fixed-topology deformation buffers. CPU readers explicitly synchronize the dirty cache; +// device updates never download the vertex array. Patch membership is CSR in local triangle indices. +struct MeshDeformationState { + MeshDeformationState(size_t* host_bytes, size_t* device_bytes) + : vertices(host_bytes, device_bytes), + input(host_bytes, device_bytes), + faces(host_bytes, device_bytes), + patch_offsets(host_bytes, device_bytes), + patch_triangles(host_bytes, device_bytes), + invalid(host_bytes, device_bytes) {} + std::shared_ptr mesh; + size_t tri_start = 0; + size_t patch_start = 0; + // Rigid meshes need no extra GPU allocations. Materialize the prepared topology only on first deformation + // or explicit center update, then reuse every allocation for subsequent calls. + void prepareDevice() { + if (device_ready) + return; + vertices.toDevice(); + faces.toDevice(); + patch_offsets.toDevice(); + patch_triangles.toDevice(); + invalid.toDevice(); + input.resizeDevice(input.size()); + device_ready = true; + } + bool device_ready = false; + bool vertices_dirty = false; + bool centers_dirty = false; + DualArray vertices; + DualArray input; + DualArray faces; + DualArray patch_offsets; + DualArray patch_triangles; + DualArray invalid; +}; +} // namespace deme +#endif diff --git a/src/algorithms/CMakeLists.txt b/src/algorithms/CMakeLists.txt index e48c1f0b..4c4ced9f 100644 --- a/src/algorithms/CMakeLists.txt +++ b/src/algorithms/CMakeLists.txt @@ -24,6 +24,7 @@ target_link_libraries( set(algorithms_interface ${CMAKE_CURRENT_SOURCE_DIR}/DEMStaticDeviceSubroutines.h ${CMAKE_CURRENT_SOURCE_DIR}/DEMOwnerDataRetrieval.h + ${CMAKE_CURRENT_SOURCE_DIR}/DEMMeshDeformation.h ) ### INTERNAL HEADERS ONLY (.h, .hpp, or .cuh) ### @@ -38,6 +39,7 @@ set(algorithms_sources ${CMAKE_CURRENT_SOURCE_DIR}/DEMCubWrappers.cu ${CMAKE_CURRENT_SOURCE_DIR}/DEMDynamicMisc.cu ${CMAKE_CURRENT_SOURCE_DIR}/DEMOwnerDataRetrieval.cu + ${CMAKE_CURRENT_SOURCE_DIR}/DEMMeshDeformation.cu ${CMAKE_CURRENT_SOURCE_DIR}/DEMMiscKernels.cu ) diff --git a/src/algorithms/DEMMeshDeformation.cu b/src/algorithms/DEMMeshDeformation.cu new file mode 100644 index 00000000..635fdbb6 --- /dev/null +++ b/src/algorithms/DEMMeshDeformation.cu @@ -0,0 +1,146 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#include "algorithms/DEMMeshDeformation.h" +#include "core/utils/Logger.hpp" + +namespace deme { +namespace { +constexpr unsigned int MESH_DEFORMATION_BLOCK = 128; + +// Validate every coordinate before any geometry is changed, including incremental overflow. +__global__ void validateVectors(const float3* input, const float3* current, size_t count, unsigned int* invalid) { + const size_t i = blockIdx.x * (size_t)blockDim.x + threadIdx.x; + if (i >= count) + return; + float3 v = input[i]; + if (current) { + v.x += current[i].x; + v.y += current[i].y; + v.z += current[i].z; + } + if (!isfinite(v.x) || !isfinite(v.y) || !isfinite(v.z)) + atomicExch(invalid, 1u); +} + +// One thread per shared vertex prevents duplicate triangle corners from accumulating increments twice. +__global__ void updateVertices(const float3* input, float3* vertices, size_t count, bool increment) { + const size_t i = blockIdx.x * (size_t)blockDim.x + threadIdx.x; + if (i >= count) + return; + float3 v = input[i]; + if (increment) { + v.x += vertices[i].x; + v.y += vertices[i].y; + v.z += vertices[i].z; + } + vertices[i] = v; +} + +// The connectivity already includes initialization's winding corrections. +__global__ void scatterVertices(const float3* vertices, + const int3* faces, + size_t count, + float3* node1, + float3* node2, + float3* node3) { + const size_t i = blockIdx.x * (size_t)blockDim.x + threadIdx.x; + if (i >= count) + return; + const int3 f = faces[i]; + node1[i] = vertices[f.x]; + node2[i] = vertices[f.y]; + node3[i] = vertices[f.z]; +} + +// One block reduces each patch's fixed triangle list. Strided loads keep large patches parallel without atomics; +// the fixed reduction tree makes repeated updates deterministic. +__global__ void patchCenters(const size_t* offsets, + const size_t* triangles, + size_t patches, + const float3* node1, + const float3* node2, + const float3* node3, + float3* centers) { + const size_t p = blockIdx.x; + const unsigned int lane = threadIdx.x; + const size_t count = offsets[p + 1] - offsets[p]; + if (patches == 1 || count == 0) { + if (lane == 0) + centers[p] = make_float3(0, 0, 0); + return; + } + __shared__ double sums[3][MESH_DEFORMATION_BLOCK]; + double x = 0, y = 0, z = 0; + for (size_t j = offsets[p] + lane; j < offsets[p + 1]; j += blockDim.x) { + const size_t t = triangles[j]; + x += ((double)node1[t].x + node2[t].x + node3[t].x) / 3.; + y += ((double)node1[t].y + node2[t].y + node3[t].y) / 3.; + z += ((double)node1[t].z + node2[t].z + node3[t].z) / 3.; + } + sums[0][lane] = x; + sums[1][lane] = y; + sums[2][lane] = z; + __syncthreads(); + for (unsigned int stride = MESH_DEFORMATION_BLOCK / 2; stride > 0; stride /= 2) { + if (lane < stride) { + sums[0][lane] += sums[0][lane + stride]; + sums[1][lane] += sums[1][lane + stride]; + sums[2][lane] += sums[2][lane + stride]; + } + __syncthreads(); + } + if (lane == 0) { + centers[p] = make_float3((float)(sums[0][0] / count), (float)(sums[1][0] / count), (float)(sums[2][0] / count)); + } +} +} // namespace + +void ValidateMeshVectors(const float3* input, + const float3* current, + size_t count, + unsigned int* invalid, + cudaStream_t stream) { + if (count) { + validateVectors<<<(count + MESH_DEFORMATION_BLOCK - 1) / MESH_DEFORMATION_BLOCK, MESH_DEFORMATION_BLOCK, 0, + stream>>>(input, current, count, invalid); + DEME_GPU_CALL(cudaGetLastError()); + } +} + +void DeformMeshVertices(const float3* input, + float3* vertices, + size_t count, + bool increment, + const int3* faces, + size_t triangles, + float3* node1, + float3* node2, + float3* node3, + cudaStream_t stream) { + if (count) { + updateVertices<<<(count + MESH_DEFORMATION_BLOCK - 1) / MESH_DEFORMATION_BLOCK, MESH_DEFORMATION_BLOCK, 0, + stream>>>(input, vertices, count, increment); + DEME_GPU_CALL(cudaGetLastError()); + } + if (triangles) { + scatterVertices<<<(triangles + MESH_DEFORMATION_BLOCK - 1) / MESH_DEFORMATION_BLOCK, MESH_DEFORMATION_BLOCK, 0, + stream>>>(vertices, faces, triangles, node1, node2, node3); + DEME_GPU_CALL(cudaGetLastError()); + } +} + +void RefreshMeshPatchCenters(const size_t* offsets, + const size_t* triangles, + size_t patches, + const float3* node1, + const float3* node2, + const float3* node3, + float3* centers, + cudaStream_t stream) { + if (patches) { + patchCenters<<>>(offsets, triangles, patches, node1, node2, node3, + centers); + DEME_GPU_CALL(cudaGetLastError()); + } +} +} // namespace deme diff --git a/src/algorithms/DEMMeshDeformation.h b/src/algorithms/DEMMeshDeformation.h new file mode 100644 index 00000000..d80a66ad --- /dev/null +++ b/src/algorithms/DEMMeshDeformation.h @@ -0,0 +1,35 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#ifndef DEME_MESH_DEFORMATION_H +#define DEME_MESH_DEFORMATION_H +#include +#include +namespace deme { +// Validate input (and the result of an incremental update) before mutating persistent geometry. +void ValidateMeshVectors(const float3* input, + const float3* current, + size_t count, + unsigned int* invalid, + cudaStream_t stream); +// Apply local vertex updates, then scatter through fixed connectivity to the solver's triangle soup. +void DeformMeshVertices(const float3* input, + float3* vertices, + size_t count, + bool increment, + const int3* faces, + size_t triangles, + float3* node1, + float3* node2, + float3* node3, + cudaStream_t stream); +// Preserve the historical local-origin convention for a single patch; otherwise average triangle centroids. +void RefreshMeshPatchCenters(const size_t* offsets, + const size_t* triangles, + size_t patches, + const float3* node1, + const float3* node2, + const float3* node3, + float3* centers, + cudaStream_t stream); +} // namespace deme +#endif diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 4bd27c7e..3ced4cbd 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -21,10 +21,11 @@ if(NOT EXISTS "${DEMEJitifyHeader}") endif() message(STATUS "${core_message} Extracting Jitify header...") -configure_file( +include("${PROJECT_SOURCE_DIR}/cmake/PrepareJitify.cmake") +deme_prepare_jitify( ${DEMEJitifyHeader} ${CMAKE_BINARY_DIR}/src/jitify/jitify.hpp - COPYONLY + "${MSVC}" ) add_library(core OBJECT) diff --git a/src/core/utils/JitHelper.cpp b/src/core/utils/JitHelper.cpp index 05868789..e25eecb7 100644 --- a/src/core/utils/JitHelper.cpp +++ b/src/core/utils/JitHelper.cpp @@ -101,6 +101,34 @@ std::filesystem::path JitHelper::KERNEL_DIR = DEMERuntimeDataHelper::data_path / std::filesystem::path JitHelper::KERNEL_INCLUDE_DIR = DEMERuntimeDataHelper::include_path; std::filesystem::path JitHelper::CACHE_DIR; +// Resolve and read runtime sources before NVRTC sees them. Empty files are valid fragments; failed I/O is not. +std::string JitHelper::loadSourceFile(const std::filesystem::path& sourcefile) { + std::error_code path_error; + const auto resolved = std::filesystem::absolute(sourcefile, path_error); + if (path_error) { + DEME_ERROR("Cannot resolve JIT source file '%s': %s", sourcefile.string().c_str(), + path_error.message().c_str()); + } + std::ifstream input(resolved); + if (!input.is_open()) { + DEME_ERROR( + "Cannot open JIT source file '%s' (missing or unreadable). Check the runtime data path and " + "DEMERuntimeDataHelper library location.", + resolved.string().c_str()); + } + + // Read through the stream API so errors are observable and no source byte is mistaken for an EOF delimiter. + std::string code; + char buffer[8192]; + while (input.read(buffer, sizeof(buffer)) || input.gcount() > 0) { + code.append(buffer, static_cast(input.gcount())); + } + if (input.bad() || !input.eof()) { + DEME_ERROR("Cannot read JIT source file '%s': I/O failure.", resolved.string().c_str()); + } + return code; +} + JitHelper::Header::Header(const std::filesystem::path& sourcefile) { this->_source = JitHelper::loadSourceFile(sourcefile); } diff --git a/src/core/utils/JitHelper.h b/src/core/utils/JitHelper.h index 7ffc6846..5dec8569 100644 --- a/src/core/utils/JitHelper.h +++ b/src/core/utils/JitHelper.h @@ -58,15 +58,8 @@ class JitHelper { static std::string toHex(uint64_t value); static std::filesystem::path resolveCacheDir(); - inline static std::string loadSourceFile(const std::filesystem::path& sourcefile) { - std::string code; - // If the file exists, read in the entire thing. - if (std::filesystem::exists(sourcefile)) { - std::ifstream input(sourcefile); - std::getline(input, code, std::string::traits_type::to_char_type(std::string::traits_type::eof())); - } - return code; - }; + // Read required runtime source text, reporting the resolved path on open or read failure. + static std::string loadSourceFile(const std::filesystem::path& sourcefile); }; class JitHelper::CachedProgram { diff --git a/src/demo/ModularTests/CMakeLists.txt b/src/demo/ModularTests/CMakeLists.txt index 2ba17ae8..ea9facdf 100644 --- a/src/demo/ModularTests/CMakeLists.txt +++ b/src/demo/ModularTests/CMakeLists.txt @@ -16,13 +16,19 @@ SET(MODULAR_TESTS DEMTest_AnalyticalCone DEMTest_AnalyticalConePour DEMTest_CombinedOwners + DEMTest_ContactOutputSnapshot DEMTest_ContactIslandFlooding DEMTest_ConeGrazingPlane DEMTest_ConeGrazingSphere DEMTest_CylinderCrossRotation DEMTest_CudaHeaderDiscovery + DEMTest_JitReflection + DEMTest_JitHeaderCache + DEMTest_JitSourceLoading DEMTest_DeviceDataRetrieval DEMTest_DeviceSelection + DEMTest_MeshDeformation + DEMTest_MeshShellContact DEMTest_MeshBox1Particle DEMTest_MeshSlidingOnSlope DEMTest_MeshTemplate diff --git a/src/demo/ModularTests/DEMTest_ContactOutputSnapshot.cpp b/src/demo/ModularTests/DEMTest_ContactOutputSnapshot.cpp new file mode 100644 index 00000000..0aaa52f4 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_ContactOutputSnapshot.cpp @@ -0,0 +1,100 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 + #include + #include +#endif + +#include "DEM/API.h" + +using namespace deme; + +// A FIFO keeps the writer blocked opening its destination until after dynamics changes the contact list. +// This tests snapshot ownership without sleeps or assumptions about background-thread scheduling. +int main() try { +#ifdef _WIN32 + std::cout << "SKIP: deterministic delayed-output test requires POSIX FIFOs.\n"; + return 0; +#else + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(4, 4, 4); + solver.SetGravitationalAcceleration(make_float3(0)); + solver.SetTimeStepSize(1.e-3); + solver.SetExpandFactor(1.e-4f); + solver.SetCDUpdateFreq(1); + solver.DefineContactForceModel("force = make_float3(0.f, 0.f, 1.f); sample_count += 1.f;"); + solver.SetContactWildcards({"sample_count"}); + solver.SetContactOutputContent({"OWNER", "GEO_ID", "FORCE", "POINT", "NORMAL", "TORQUE", "CNT_WILDCARD"}); + auto material = solver.LoadMaterial({{"E", 1.e7}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", 0.4}}); + auto sphere = solver.LoadSphereType(1, 0.1f, material); + auto spheres = + solver.AddClumps(sphere, std::vector{make_float3(-0.5f, 0, 0.09f), make_float3(0.5f, 0, 0.115f)}); + spheres->SetVel(std::vector{make_float3(0), make_float3(0, 0, -1)}); + solver.AddBCPlane(make_float3(0), make_float3(0, 0, 1), material); + solver.Initialize(); + solver.DoDynamicsThenSync(1.e-3); + const size_t before = solver.GetNumContacts(); + if (before != 1) + throw std::runtime_error("Expected one initial contact, got " + std::to_string(before)); + + const auto dir = std::filesystem::temp_directory_path() / ("deme_contact_snapshot_" + std::to_string(getpid())); + std::filesystem::create_directory(dir); + // Exercise normal CSV, the binary-to-CSV fallback, and a force threshold that filters out every row. + // Each output must match a synchronous reference from the same frame even after the solver advances. + const auto check_delayed_output = [&](OUTPUT_FORMAT format, float threshold, double duration) { + solver.SetContactOutputFormat(format); + solver.WriteContactFile((dir / "reference.csv").string(), threshold); + // Snapshot ownership permits dynamics overlap, but the reference file must finish before we read it. + solver.WaitForPendingOutput(); + std::ifstream reference_file(dir / "reference.csv"); + std::ostringstream reference; + reference << reference_file.rdbuf(); + if (!reference_file || reference.str().empty()) + throw std::runtime_error("Cannot read reference contact output"); + const auto pipe = dir / "delayed.csv"; + if (mkfifo(pipe.c_str(), 0600) != 0) + throw std::runtime_error("Cannot create delayed-output FIFO"); + solver.WriteContactFile(pipe.string(), threshold); + + // Always release the writer before rethrowing a dynamics error, so solver destruction cannot hang on join(). + std::exception_ptr dynamics_error; + try { + solver.DoDynamicsThenSync(duration); + } catch (...) { + dynamics_error = std::current_exception(); + } + std::ifstream delayed_file(pipe); + std::ostringstream delayed; + delayed << delayed_file.rdbuf(); + // Join the writer before cleaning up its destination; dynamics already ran while output was pending. + solver.WaitForPendingOutput(); + if (dynamics_error) + std::rethrow_exception(dynamics_error); + if (delayed.str() != reference.str()) + throw std::runtime_error("Delayed contact output differs from the requested frame"); + std::filesystem::remove(pipe); + }; + check_delayed_output(OUTPUT_FORMAT::CSV, DEME_TINY_FLOAT, 0.05); + const size_t after = solver.GetNumContacts(); + if (after <= before) + throw std::runtime_error("Contact list did not grow during delayed output"); + check_delayed_output(OUTPUT_FORMAT::BINARY, -1.f, 0.01); + check_delayed_output(OUTPUT_FORMAT::CSV, 1.e6f, 0.01); + std::filesystem::remove_all(dir); + std::cout << "PASS: delayed contact output preserves its snapshot while contacts grow from " << before << " to " + << after << ".\n"; + return 0; +#endif +} catch (const std::exception& e) { + std::cerr << "FAIL: " << e.what() << '\n'; + return 1; +} diff --git a/src/demo/ModularTests/DEMTest_JitHeaderCache.cpp b/src/demo/ModularTests/DEMTest_JitHeaderCache.cpp new file mode 100644 index 00000000..23605ea7 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_JitHeaderCache.cpp @@ -0,0 +1,109 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include +#include +#include + +#include "jitify/jitify.hpp" + +namespace { +// Read the serialized records directly so successful saves cannot be mistaken for in-memory cache reuse. +void checkCache(const std::filesystem::path& path, uint64_t expected_count) { + std::ifstream input(path, std::ios::binary); + std::string magic, signature; + jitify::detail::read_string_record(input, &magic); + jitify::detail::read_string_record(input, &signature); + uint64_t count = 0; + input.read(reinterpret_cast(&count), sizeof(count)); + if (!input || magic != "JITIFY_HEADER_CACHE_V2" || signature != "cache-test" || count != expected_count) + throw std::runtime_error("Persistent header cache did not contain the expected saved records"); + for (uint64_t i = 0; i < count; ++i) { + std::string name, source, fullpath; + jitify::detail::read_string_record(input, &name); + jitify::detail::read_string_record(input, &source); + jitify::detail::read_string_record(input, &fullpath); + if (!input || source != "// " + name) + throw std::runtime_error("Persistent header cache contained an incomplete record"); + } +} + +// Select a private cache file without touching a user's existing persistent cache. +void setCachePath(const std::filesystem::path& path) { +#ifdef _WIN32 + _putenv_s("DEME_PERSISTENT_JITIFY_CACHE", path.string().c_str()); +#else + setenv("DEME_PERSISTENT_JITIFY_CACHE", path.string().c_str(), 1); +#endif +} +} // namespace + +// Exercise creation, replacement, failed publication and retry without constructing a CUDA context. +int main() { + const auto directory = + std::filesystem::temp_directory_path() / + ("deme-header-cache-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + try { + std::filesystem::create_directories(directory); + const auto cache_path = directory / "headers.bin"; + setCachePath(cache_path); + auto& sources = jitify::detail::global_header_source_cache(); + sources.clear(); + jitify::detail::global_header_fullpath_cache().clear(); + jitify::detail::persistent_header_cache_saved_count() = 0; + sources["first.h"] = "// first.h"; + jitify::detail::save_persistent_header_cache_if_needed("cache-test"); + checkCache(cache_path, 1); + sources["second.h"] = "// second.h"; + jitify::detail::save_persistent_header_cache_if_needed("cache-test"); + checkCache(cache_path, 2); + + // Windows input streams deny delete sharing. Hold the old cache open to force replacement to fail; + // POSIX permits replacing open files, so use an existing directory as the blocked destination there. +#ifdef _WIN32 + std::ifstream blocked_file(cache_path, std::ios::binary); + if (!blocked_file) + throw std::runtime_error("Could not hold the old cache open for the failure test"); +#else + const auto blocked_path = directory / "blocked"; + std::filesystem::create_directory(blocked_path); + setCachePath(blocked_path); +#endif + sources["third.h"] = "// third.h"; + jitify::detail::save_persistent_header_cache_if_needed("cache-test"); +#ifdef _WIN32 + blocked_file.close(); +#else + if (!std::filesystem::is_directory(blocked_path)) + throw std::runtime_error("Failed publication changed the blocked destination"); +#endif + if (jitify::detail::persistent_header_cache_saved_count() != 2) + throw std::runtime_error("Failed publication changed the destination or marked the cache saved"); + checkCache(cache_path, 2); + setCachePath(cache_path); + jitify::detail::save_persistent_header_cache_if_needed("cache-test"); + checkCache(cache_path, 3); + if (std::filesystem::exists(cache_path.string() + ".tmp")) + throw std::runtime_error("Successful publication left its temporary file behind"); + + // Loading after clearing process state verifies that a fresh process can recover every expanded record. + sources.clear(); + jitify::detail::persistent_header_cache_saved_count() = 0; + jitify::detail::persistent_header_cache_loaded() = false; + jitify::detail::load_persistent_header_cache_once("cache-test"); + if (sources.size() != 3 || sources.at("third.h") != "// third.h") + throw std::runtime_error("Expanded header cache did not reload correctly"); + std::filesystem::remove_all(directory); + std::cout << "PASS: header-cache creation, replacement, failed publication, retry and reload.\n"; + return 0; + } catch (const std::exception& error) { + std::error_code cleanup_error; + std::filesystem::remove_all(directory, cleanup_error); + std::cerr << "FAIL: " << error.what() << '\n'; + return 1; + } +} diff --git a/src/demo/ModularTests/DEMTest_JitReflection.cpp b/src/demo/ModularTests/DEMTest_JitReflection.cpp new file mode 100644 index 00000000..9db1420a --- /dev/null +++ b/src/demo/ModularTests/DEMTest_JitReflection.cpp @@ -0,0 +1,66 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include +#include +#include + +#include "jitify/jitify.hpp" + +namespace { +// Include different argument shapes so workers exercise the common demangler across multiple reflected types. +std::array reflectTypes() { + return {jitify::reflection::reflect(), jitify::reflection::reflect(), + jitify::reflection::reflect(), jitify::reflection::reflect(), + jitify::reflection::reflect(), jitify::reflection::reflect()}; +} +} // namespace + +// No CUDA context is needed: under MSVC this stresses DbgHelp directly, as concurrent dT/kT launches do. +int main() try { + const auto expected = reflectTypes(); + for (const auto& name : expected) { + if (name.empty()) + throw std::runtime_error("Empty reflected type name"); + } + std::atomic ready{0}; + std::atomic start{false}; + std::atomic failed{false}; + constexpr unsigned int worker_count = 8; + std::vector workers; + for (unsigned int i = 0; i < worker_count; ++i) { + workers.emplace_back([&]() { + ++ready; + while (!start.load()) + std::this_thread::yield(); + try { + for (unsigned int iteration = 0; iteration < 2000; ++iteration) { + if (reflectTypes() != expected) + failed = true; + } + } catch (...) { + failed = true; + } + }); + } + while (ready.load() != worker_count) + std::this_thread::yield(); + start = true; + for (auto& worker : workers) + worker.join(); + if (failed.load()) + throw std::runtime_error("Concurrent type reflection changed names or threw an exception"); +#ifdef _MSC_VER + std::cout << "PASS: concurrent MSVC/DbgHelp type reflection.\n"; +#else + std::cout << "PASS: concurrent native type reflection (DbgHelp coverage requires MSVC).\n"; +#endif + return 0; +} catch (const std::exception& error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return 1; +} diff --git a/src/demo/ModularTests/DEMTest_JitSourceLoading.cpp b/src/demo/ModularTests/DEMTest_JitSourceLoading.cpp new file mode 100644 index 00000000..abc824e5 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_JitSourceLoading.cpp @@ -0,0 +1,74 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include +#include + +#include "core/utils/JitHelper.h" +#include "core/utils/Logger.hpp" + +namespace { +// Both header and kernel entry points must reject failed I/O before any CUDA/NVRTC work, with a usable path. +void expectSourceError(const std::filesystem::path& path, bool kernel = false) { + try { + if (kernel) + JitHelper::buildProgram("missing_source_probe", path); + else + JitHelper::Header{path}; + } catch (const deme::SolverException& error) { + if (std::string(error.what()).find(std::filesystem::absolute(path).string()) == std::string::npos) + throw std::runtime_error("Source diagnostic omitted the absolute path"); + return; + } + throw std::runtime_error("Invalid source path was silently accepted: " + path.string()); +} +} // namespace + +int main() { + const auto directory = + std::filesystem::temp_directory_path() / + ("deme-source-loading-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + try { + std::filesystem::create_directories(directory); + const auto source = directory / "header.cuh"; + // Exercise multiple read buffers and preserve bytes that are not a character representation of EOF. + const std::string contents = std::string(20000, 'x') + char(0xff) + "\n// trailing source\n"; + std::ofstream(source) << contents; + JitHelper::Header header(source); + if (header.getSource() != contents) + throw std::runtime_error("Source text was truncated or changed"); + const auto empty = directory / "empty.cuh"; + std::ofstream(empty).close(); + if (!JitHelper::Header(empty).getSource().empty()) + throw std::runtime_error("Empty source fragment was not preserved"); + + std::cout << "Checking expected source-loading errors (missing, directory and unreadable paths)." << std::endl; + expectSourceError(directory / "missing.cuh"); + expectSourceError(directory / "missing.cu", true); + // Opening a directory either fails immediately or fails during reading, depending on the platform. + expectSourceError(directory); + + // Permission enforcement varies (notably Windows permissions and privileged POSIX users). + std::error_code permission_error; + const auto saved_permissions = std::filesystem::status(source).permissions(); + std::filesystem::permissions(source, std::filesystem::perms::none, permission_error); + if (!permission_error && !std::ifstream(source).is_open()) { + expectSourceError(source); + } else { + std::cout << "SKIP: unreadable-file check (read permissions are not enforced here).\n"; + } + std::filesystem::permissions(source, saved_permissions, permission_error); + std::filesystem::remove_all(directory); + std::cout << "PASS: JIT source loading preserves content and empty fragments, and reports failed I/O.\n"; + return 0; + } catch (const std::exception& error) { + std::error_code cleanup_error; + std::filesystem::remove_all(directory, cleanup_error); + std::cerr << "FAIL: " << error.what() << '\n'; + return 1; + } +} diff --git a/src/demo/ModularTests/DEMTest_MeshDeformation.cpp b/src/demo/ModularTests/DEMTest_MeshDeformation.cpp new file mode 100644 index 00000000..557a03d3 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_MeshDeformation.cpp @@ -0,0 +1,339 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include +#include +#include +#include +#include + +#include "DEM/API.h" +#include "core/utils/CudaDebugSync.hpp" + +using namespace deme; +namespace { +void require(bool condition, const char* message) { + if (!condition) + throw std::runtime_error(message); +} +bool approxEqual(float3 a, float3 b) { + return std::abs(a.x - b.x) < 2.e-5f && std::abs(a.y - b.y) < 2.e-5f && std::abs(a.z - b.z) < 2.e-5f; +} +void compare(const std::vector& actual, const std::vector& expected, const char* message) { + require(actual.size() == expected.size(), message); + for (size_t i = 0; i < actual.size(); ++i) + require(approxEqual(actual[i], expected[i]), message); +} + +// Keep input allocations alive across calls, as an external GPU solid-mechanics solver would. +struct DeviceInput { + int device; + float3* data = nullptr; + DeviceInput(size_t count, int device) : device(device) { + ScopedCudaDevice scope(device); + DEME_GPU_CALL(cudaMalloc(reinterpret_cast(&data), count * sizeof(float3))); + } + ~DeviceInput() { + ScopedCudaDevice scope(device); + cudaFree(data); + } + void upload(const std::vector& values) { + ScopedCudaDevice scope(device); + DEME_GPU_CALL(cudaMemcpy(data, values.data(), values.size() * sizeof(float3), cudaMemcpyHostToDevice)); + } +}; + +// Reading through the tracker must download both GPU vertices and the actual relPosPatch values used by forces. +void checkAutomatic(const std::shared_ptr& tracker, const std::vector& expected) { + auto mesh = tracker->GetMesh(); + compare(mesh->GetCoordsVertices(), expected, "device/host vertex mismatch"); + compare(mesh->GetPatchLocations(), mesh->ComputePatchLocations(), "automatic GPU patch centers are stale"); +} +// A cube below an upward-facing open plane produces deep back-face triangle pairings. Fresh centers reject these; +// centers left at the cube's previous local position admit them. The open plane cannot mask that mistake with its +// own center guard. Compare deformation against loading the target geometry directly, without changing the rule. +float contactForceAfterDeformation(bool deform, bool refresh) { + const bool trace = std::getenv("DEME_MESH_TRACE") != nullptr; + if (trace) + std::cerr << "CONTACT CASE deform=" << deform << " refresh=" << refresh << ": construct\n"; + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(8, 8, 8); + solver.SetGravitationalAcceleration(make_float3(0)); + solver.SetMeshUniversalContact(true); + solver.SetCDUpdateFreq(1); + solver.SetInitBinNumTarget(8); + solver.SetErrorOutAvgContacts(10000); + solver.SetTimeStepSize(1.e-5); + solver.DefineContactForceModel("force = make_float3(0.f, 0.f, static_cast(overlapDepth));"); + auto material = solver.LoadMaterial({{"E", 1.e7}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", 0.4}, {"Crr", 0.0}}); + auto cube = solver.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cube.obj").string(), material); + cube->Scale(0.2f); + std::vector patches(cube->GetNumTriangles()); + for (size_t i = 0; i < patches.size(); ++i) + patches[i] = i % 2; + cube->SetPatchIDs(patches); + cube->SetMaterial(material); + cube->SetFamily(100); + solver.SetFamilyFixed(100); + const auto target = cube->GetCoordsVertices(); + if (deform) { + for (auto& v : cube->m_vertices) + v.x += 2.f; + } + auto tracker = solver.Track(cube); + auto plane = solver.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/plane_20by20.obj").string(), material); + plane->Scale(0.05f); + plane->SetInitPos(make_float3(0, 0, 0.096f)); + plane->SetFamily(101); + solver.SetFamilyFixed(101); + solver.Initialize(); + require(cube->IsWatertight() && !plane->IsWatertight(), "contact regression mesh topology changed"); + if (deform) { + const int device = solver.GetGPUDeviceIDs().front(); + DeviceInput input(target.size(), device); + input.upload(target); + tracker->UpdateMeshFromDevice(input.data, device, true, refresh); + } + if (trace) + std::cerr << "CONTACT CASE: initialized and deformed; starting dynamics\n"; + solver.SetTriTriPenetration(0.4f); + solver.DoDynamicsThenSync(1.e-4); + if (trace) + std::cerr << "CONTACT CASE: dynamics complete; reading forces\n"; + std::vector points, forces; + tracker->GetContactForces(points, forces); + float total = 0; + for (const auto& force : forces) + total += length(force); + if (trace) + std::cerr << "CONTACT CASE: force=" << total << "; destroying solver\n"; + return total; +} +} // namespace + +// Cover shared-node increments, deferred centers, manual policy, lazy readers, and host/device interleaving. +int main() try { + // Opt-in diagnostics preserve ordinary asynchronous execution for reproducing timing-sensitive failures. + deme::SetCudaDebugSyncEnabled(std::getenv("DEME_MESH_DEBUG_SYNC") != nullptr); + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(40, 40, 40); + solver.SetGravitationalAcceleration(make_float3(0)); + solver.SetTimeStepSize(1.e-5); + auto material = solver.LoadMaterial({{"E", 1.e7}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", 0.4}, {"Crr", 0.0}}); + auto sphere = solver.LoadSphereType(1, 0.1f, material); + solver.AddClumps(sphere, std::vector{make_float3(0, 0, 10)}); + auto mesh = solver.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/plane_20by20.obj").string(), material); + mesh->Scale(0.1f); + mesh->SetEachTriangleAsPatch(); + mesh->SetMaterial(material); + mesh->SetFamily(1); + solver.SetFamilyFixed(1); + auto tracker = solver.Track(mesh); + auto single = solver.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/plane_20by20.obj").string(), material); + // Deliberately reverse cached winding; initialization must restore the supplied +Z normals, and deformation + // must keep that corrected triangle order rather than scattering the raw face order back onto the GPU. + single->UseNormals(true); + for (auto& face : single->m_face_v_indices) + std::swap(face.y, face.z); + single->SetInitPos(make_float3(0, 0, -5)); + single->SetFamily(1); + single->SetPatchLocations({make_float3(1, 2, 3)}); + auto single_tracker = solver.Track(single); + solver.Initialize(); + const int device = solver.GetGPUDeviceIDs().front(); + const size_t count = mesh->GetNumNodes(); + require(mesh->GetNumPatches() > 1, "test requires multiple triangle patches"); + DeviceInput input(std::max(count, (size_t)mesh->GetNumPatches()), device); + auto expected = mesh->GetCoordsVertices(); + for (size_t i = 0; i < count; ++i) { + expected[i].x += 0.25f * i; + expected[i].z += 0.1f * i; + } + input.upload(expected); + tracker->UpdateMeshFromDevice(input.data, device); + checkAutomatic(tracker, expected); + + const auto old_centers = mesh->GetPatchLocations(); + std::vector increment(count, make_float3(0.125f, -0.25f, 0.375f)); + input.upload(increment); + for (int step = 0; step < 8; ++step) { + tracker->UpdateMeshByIncrementFromDevice(input.data, device, true, false); + for (auto& v : expected) + v += increment[0]; + } + compare(tracker->GetMesh()->GetPatchLocations(), old_centers, "disabled refresh changed centers"); + tracker->UpdateMeshByIncrementFromDevice(input.data, device, false, true); + for (auto& v : expected) + v += increment[0]; + checkAutomatic(tracker, expected); + + // Do not read the cache between device and host updates: increments must use GPU-authoritative vertices. + tracker->UpdateMeshByIncrementFromDevice(input.data, device); + tracker->UpdateMeshByIncrement(increment); + for (auto& v : expected) + v += increment[0] * 2.f; + checkAutomatic(tracker, expected); + expected[0].z += 0.75f; + tracker->UpdateMesh(expected); + checkAutomatic(tracker, expected); + + // Exercise each CPU entry point directly. This is the first mesh, so its triangle start is zero. + // Skipping refresh must still update vertices; resuming must include every skipped deformation. + for (int api = 0; api < 4; ++api) { + auto update = [&](bool refresh) { + for (auto& v : expected) + v += increment[0]; + switch (api) { + case 0: + tracker->UpdateMesh(expected, refresh); + break; + case 1: + tracker->UpdateMeshByIncrement(increment, refresh); + break; + case 2: + solver.SetTriNodeRelPos(tracker->GetOwnerID(), 0, expected, refresh); + break; + case 3: + solver.UpdateTriNodeRelPos(tracker->GetOwnerID(), 0, increment, refresh); + break; + } + }; + const auto saved_centers = mesh->GetPatchLocations(); + update(false); + update(false); + compare(mesh->GetCoordsVertices(), expected, "CPU skipped refresh failed to update vertices"); + compare(mesh->GetPatchLocations(), saved_centers, "CPU disabled refresh changed centers"); + update(true); + checkAutomatic(tracker, expected); + const std::vector explicit_centers(mesh->GetNumPatches(), make_float3(4, 5, 6)); + tracker->UpdateMeshPatchLocations(explicit_centers); + update(false); + update(true); + compare(mesh->GetCoordsVertices(), expected, "CPU manual-center update failed to update vertices"); + compare(mesh->GetPatchLocations(), explicit_centers, "CPU toggle overwrote explicit centers"); + tracker->UseAutomaticMeshPatchLocations(); + checkAutomatic(tracker, expected); + } + // Omitted arguments on the public solver methods must retain the default refresh behavior too. + for (auto& v : expected) + v += increment[0]; + solver.SetTriNodeRelPos(tracker->GetOwnerID(), 0, expected); + checkAutomatic(tracker, expected); + solver.UpdateTriNodeRelPos(tracker->GetOwnerID(), 0, increment); + for (auto& v : expected) + v += increment[0]; + checkAutomatic(tracker, expected); + + std::vector manual(mesh->GetNumPatches(), make_float3(4, 5, 6)); + tracker->UpdateMeshPatchLocations(manual); + input.upload(increment); + tracker->UpdateMeshByIncrementFromDevice(input.data, device, true, true); + for (auto& v : expected) + v += increment[0]; + compare(tracker->GetMesh()->GetPatchLocations(), manual, "deformation overwrote user centers"); + manual[0] = make_float3(7, 8, 9); + input.upload(manual); + tracker->UpdateMeshPatchLocationsFromDevice(input.data, device); + compare(tracker->GetMesh()->GetPatchLocations(), manual, "device center setter did not update GPU centers"); + tracker->UseAutomaticMeshPatchLocations(); + require(!mesh->ArePatchLocationsExplicitlySet(), "automatic policy did not resume"); + checkAutomatic(tracker, expected); + + auto single_nodes = single->GetCoordsVertices(); + for (auto& v : single_nodes) + v.z += 1; + single_tracker->UpdateMesh(single_nodes); + compare(single->GetPatchLocations(), {make_float3(1, 2, 3)}, "setup explicit center was overwritten"); + single_tracker->UseAutomaticMeshPatchLocations(); + compare(single->GetPatchLocations(), {make_float3(0)}, "single-patch origin convention changed"); + + // Visualization and async output are observation boundaries even without an intervening GetMesh call. + input.upload(increment); + tracker->UpdateMeshByIncrementFromDevice(input.data, device); + for (auto& v : expected) + v += increment[0]; + const auto scene = solver.GetVisualizationScene(); + for (const auto& triangle : scene.triangles) { + if (triangle.owner == single_tracker->GetOwnerID()) { + require(cross(triangle.b - triangle.a, triangle.c - triangle.a).z > 0, + "deformation lost initialization's winding correction"); + } + } + require(approxEqual(scene.triangles.front().a, expected[mesh->GetIndicesVertexes()[0].x]), + "stale visualization geometry"); + tracker->UpdateMeshByIncrementFromDevice(input.data, device); + for (auto& v : expected) + v += increment[0]; + auto world = tracker->GetMeshNodesGlobal(); + const auto position = tracker->Pos(); + auto expected_world = expected; + for (auto& v : expected_world) + v += position; + compare(world, expected_world, "global node getter returned stale coordinates"); + // Dirty the cache once more so output itself must perform the synchronization. + tracker->UpdateMeshByIncrementFromDevice(input.data, device); + for (auto& v : expected) + v += increment[0]; + const auto output = std::filesystem::temp_directory_path() / "deme_mesh_deformation_test.vtk"; + solver.SetMeshOutputFormat("VTK"); + solver.WriteMeshFile(output.string()); + solver.WaitForPendingOutput(); + compare(mesh->GetCoordsVertices(), expected, "output failed to synchronize mesh vertices"); + require(std::filesystem::file_size(output) > 0, "mesh output is empty"); + std::filesystem::remove(output); + + // Input validation must reject non-finite values before mutating geometry. + auto invalid = expected; + invalid[0].x = std::numeric_limits::quiet_NaN(); + input.upload(invalid); + bool rejected = false; + try { + tracker->UpdateMeshFromDevice(input.data, device); + } catch (const std::exception&) { + rejected = true; + } + require(rejected, "non-finite device coordinates were accepted"); + checkAutomatic(tracker, expected); + + // Runtime insertion must not restore original host triangle/center arrays over device deformation. + input.upload(increment); + tracker->UpdateMeshByIncrementFromDevice(input.data, device); + for (auto& v : expected) + v += increment[0]; + solver.ClearCache(); + solver.AddClumps(sphere, std::vector{make_float3(3, 0, 10)}); + solver.Update(); + checkAutomatic(tracker, expected); + solver.DoDynamicsThenSync(1.e-5); + checkAutomatic(tracker, expected); + + int devices = 0; + DEME_GPU_CALL(cudaGetDeviceCount(&devices)); + if (devices > 1) { + DeviceInput remote(count, (device + 1) % devices); + remote.upload(increment); + tracker->UpdateMeshByIncrementFromDevice(remote.data, remote.device); + for (auto& v : expected) + v += increment[0]; + checkAutomatic(tracker, expected); + } else { + std::cout << "SKIP: remote-device update (only one CUDA device).\n"; + } + const float reference_force = contactForceAfterDeformation(false, true); + const float refreshed_force = contactForceAfterDeformation(true, true); + const float stale_force = contactForceAfterDeformation(true, false); + std::cout << "Contact guard forces: reference=" << reference_force << ", refreshed=" << refreshed_force + << ", stale=" << stale_force << '\n'; + require(std::abs(reference_force) < 1.e-5f, "reference failed to reject deep back-face contacts"); + require(std::abs(reference_force - refreshed_force) < 1.e-5f, "deformation changed filtered patch forces"); + require(std::abs(stale_force - reference_force) > 1.e-3f, + "contact regression did not exercise stale-center rejection"); + std::cout << "PASS: mesh device deformation, center policies, mixed updates, output, and insertion.\n"; + return 0; +} catch (const std::exception& e) { + std::cerr << "FAIL: " << e.what() << '\n'; + return 1; +} diff --git a/src/demo/ModularTests/DEMTest_MeshShellContact.cpp b/src/demo/ModularTests/DEMTest_MeshShellContact.cpp new file mode 100644 index 00000000..a88b6950 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_MeshShellContact.cpp @@ -0,0 +1,71 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include + +#include "DEM/API.h" + +using namespace deme; + +namespace { +// The spheres clear the bare surfaces by 0.03; shell thickness yields penetrations of 0.02 and 0.03. +// A tiny fixed search margin ensures shell thickness itself must reach contact detection. +void checkShellForce(const std::shared_ptr& tracker, float expected = 0.02f) { + std::vector points, forces; + tracker->GetContactForces(points, forces); + float total = 0.f; + for (const auto& force : forces) + total += length(force); + if (std::abs(total - expected) > 2.e-4f) + throw std::runtime_error("Expected shell contact force " + std::to_string(expected) + ", got " + + std::to_string(total)); +} +} // namespace + +// Cover initial shell metadata and its preservation when clump insertion grows the owner arrays. +int main() try { + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(12, 12, 12); + solver.SetGravitationalAcceleration(make_float3(0)); + solver.SetTimeStepSize(1.e-5); + solver.SetExpandFactor(1.e-4f); + solver.SetCDUpdateFreq(1); + // Separate the bare surface and sphere AABBs into disjoint bins; only shell expansion bridges the gap. + solver.SetInitBinSize(0.025); + solver.DefineContactForceModel("force = make_float3(0.f, 0.f, static_cast(overlapDepth));"); + auto material = solver.LoadMaterial({{"E", 1.e7}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", 0.4}, {"Crr", 0.0}}); + auto sphere = solver.LoadSphereType(1, 0.05f, material); + auto spheres = solver.AddClumps(sphere, std::vector{make_float3(-1.75f, 0.125f, 0.08f)}); + spheres->SetFamily(1); + solver.SetFamilyFixed(1); + solver.SetFamilyFixed(100); + auto addShell = [&](float x, float thickness) { + auto mesh = solver.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/plane_20by20.obj").string(), material); + mesh->Scale(0.05f); + mesh->SetInitPos(make_float3(x, 0, 0)); + mesh->SetShellThickness(thickness); + mesh->SetFamily(100); + return solver.Track(mesh); + }; + auto first = addShell(-2.f, 0.1f); + auto second = addShell(2.f, 0.12f); + solver.Initialize(); + solver.DoDynamicsThenSync(1.e-5); + checkShellForce(first); + checkShellForce(second, 0.f); + + solver.ClearCache(); + auto added = solver.AddClumps(sphere, std::vector{make_float3(2.25f, 0.125f, 0.08f)}); + added->SetFamily(1); + solver.Update(); + solver.DoDynamicsThenSync(1.e-5); + checkShellForce(first); + checkShellForce(second, 0.03f); + std::cout << "PASS: shell thickness reaches contact detection at initialization and after insertion.\n"; + return 0; +} catch (const std::exception& e) { + std::cerr << "FAIL: " << e.what() << '\n'; + return 1; +} diff --git a/src/demo/ModularTests/DEMTest_VisualizationScene.cpp b/src/demo/ModularTests/DEMTest_VisualizationScene.cpp index 3c936b95..607632dd 100644 --- a/src/demo/ModularTests/DEMTest_VisualizationScene.cpp +++ b/src/demo/ModularTests/DEMTest_VisualizationScene.cpp @@ -12,7 +12,7 @@ void require(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } -bool near(float a, float b) { +bool approxEqual(float a, float b) { return std::abs(a - b) < 1.e-4f; } @@ -23,8 +23,8 @@ float3 transform(float3 p, float3 position, float4 q) { position.y + p.y + q.w * t.y + q.z * t.x - q.x * t.z, position.z + p.z + q.w * t.z + q.x * t.y - q.y * t.x); } -bool near(float3 a, float3 b) { - return near(a.x, b.x) && near(a.y, b.y) && near(a.z, b.z); +bool approxEqual(float3 a, float3 b) { + return approxEqual(a.x, b.x) && approxEqual(a.y, b.y) && approxEqual(a.z, b.z); } void compare(DEMSolver& solver, const DEMVisualizationScene& scene, DEMVisualizationFrame& frame) { solver.GetVisualizationFrame(frame, true); @@ -33,10 +33,10 @@ void compare(DEMSolver& solver, const DEMVisualizationScene& scene, DEMVisualiza require(scene.spheres.size() == snapshot.spheres.size(), "sphere count mismatch"); for (size_t i = 0; i < scene.spheres.size(); ++i) { const auto& s = scene.spheres[i]; - require(near(transform(s.offset, frame.positions[s.owner], frame.orientations[s.owner]), - snapshot.spheres[i].position), + require(approxEqual(transform(s.offset, frame.positions[s.owner], frame.orientations[s.owner]), + snapshot.spheres[i].position), "cached sphere transform mismatch"); - require(near(s.radius, snapshot.spheres[i].radius), "sphere radius mismatch"); + require(approxEqual(s.radius, snapshot.spheres[i].radius), "sphere radius mismatch"); require(frame.families[s.owner] == snapshot.spheres[i].family, "family mismatch"); } require(scene.triangles.size() == snapshot.triangles.size(), "triangle count mismatch"); @@ -44,9 +44,9 @@ void compare(DEMSolver& solver, const DEMVisualizationScene& scene, DEMVisualiza const auto& t = scene.triangles[i]; auto p = frame.positions[t.owner]; auto q = frame.orientations[t.owner]; - require(near(transform(t.a, p, q), snapshot.triangles[i].a) && - near(transform(t.b, p, q), snapshot.triangles[i].b) && - near(transform(t.c, p, q), snapshot.triangles[i].c), + require(approxEqual(transform(t.a, p, q), snapshot.triangles[i].a) && + approxEqual(transform(t.b, p, q), snapshot.triangles[i].b) && + approxEqual(transform(t.c, p, q), snapshot.triangles[i].c), "cached triangle transform mismatch"); } } @@ -87,7 +87,7 @@ int main() try { compare(solver, scene, frame); require(frame.positions.data() == storage, "unchanged frame reallocated owner storage"); auto owner = track->GetOwnerID(); - require(near(frame.velocities[owner], make_float3(0.1f, 0.2f, 0.3f)), "velocity field mismatch"); + require(approxEqual(frame.velocities[owner], make_float3(0.1f, 0.2f, 0.3f)), "velocity field mismatch"); solver.GetVisualizationFrame(frame, false); require(frame.velocities.empty(), "unrequested velocities retained"); solver.DoDynamicsThenSync(1.e-4); diff --git a/src/demo/ModularTests/DEMTest_VisualizationSnapshot.cpp b/src/demo/ModularTests/DEMTest_VisualizationSnapshot.cpp index caec708e..aa52b4cd 100644 --- a/src/demo/ModularTests/DEMTest_VisualizationSnapshot.cpp +++ b/src/demo/ModularTests/DEMTest_VisualizationSnapshot.cpp @@ -16,7 +16,7 @@ using namespace deme; namespace { -bool near(float actual, float expected, float tolerance = 1.0e-5f) { +bool approxEqual(float actual, float expected, float tolerance = 1.0e-5f) { return std::abs(actual - expected) <= tolerance; } @@ -48,8 +48,8 @@ int main() { return 1; } const auto& rendered_sphere = sphere_snapshot.spheres.front(); - if (!near(rendered_sphere.position.x, 1.0f) || !near(rendered_sphere.position.y, 2.0f) || - !near(rendered_sphere.position.z, 3.0f) || !near(rendered_sphere.radius, 0.25f)) { + if (!approxEqual(rendered_sphere.position.x, 1.0f) || !approxEqual(rendered_sphere.position.y, 2.0f) || + !approxEqual(rendered_sphere.position.z, 3.0f) || !approxEqual(rendered_sphere.radius, 0.25f)) { std::cerr << "FAIL: visualization sphere does not match its initialized world transform." << std::endl; return 1; } @@ -63,6 +63,8 @@ int main() { std::ifstream vtk_file(vtk_path); std::ostringstream vtk_contents; vtk_contents << vtk_file.rdbuf(); + // Close input streams before removing their files; Windows denies deletion while a stream is open. + vtk_file.close(); std::filesystem::remove(vtk_path); const std::string vtk_text = vtk_contents.str(); if (vtk_text.find("DATASET POLYDATA") == std::string::npos || @@ -80,6 +82,7 @@ int main() { std::ifstream analytical_vtk_file(analytical_vtk_path); std::ostringstream analytical_vtk_contents; analytical_vtk_contents << analytical_vtk_file.rdbuf(); + analytical_vtk_file.close(); std::filesystem::remove(analytical_vtk_path); const std::string analytical_vtk_text = analytical_vtk_contents.str(); if (analytical_vtk_text.find("DATASET POLYDATA") == std::string::npos || @@ -97,6 +100,7 @@ int main() { std::ifstream mesh_vtk_file(mesh_vtk_path); std::ostringstream mesh_vtk_contents; mesh_vtk_contents << mesh_vtk_file.rdbuf(); + mesh_vtk_file.close(); std::filesystem::remove(mesh_vtk_path); const std::string mesh_vtk_text = mesh_vtk_contents.str(); const std::vector required_mesh_fields = {"CELL_DATA 2", diff --git a/src/jitify/jitify.hpp b/src/jitify/jitify.hpp index 51e2bff2..520cfc2e 100644 --- a/src/jitify/jitify.hpp +++ b/src/jitify/jitify.hpp @@ -106,7 +106,8 @@ #include #include #include -#if JITIFY_THREAD_SAFE +// MSVC also needs this for DbgHelp, independently of the cache thread-safety setting. +#if JITIFY_THREAD_SAFE || defined(_MSC_VER) #include #endif @@ -771,6 +772,10 @@ inline std::string demangle_cuda_symbol(const char* mangled_name) { return mangled_name; } inline std::string demangle_native_type(const std::type_info& typeinfo) { + // DEME serializes DbgHelp across all reflected types and worker threads. Its API is not thread-safe, + // independently of Jitify's cache locks. Keep this in the non-template MSVC function so there is one mutex. + static std::mutex dbghelp_mutex; + std::lock_guard lock(dbghelp_mutex); // Get the decorated name and skip over the leading '.'. const char* decorated_name = typeinfo.raw_name() + 1; char undecorated_name[4096]; @@ -2999,8 +3004,16 @@ inline void save_persistent_header_cache_if_needed(const std::string& signature) } out.close(); if (out) { - std::rename(tmp_path.c_str(), path.c_str()); - persistent_header_cache_saved_count() = source_cache.size(); + // Windows CRT rename cannot replace an existing file. Publish the completed temporary file without + // deleting the old cache first, and leave the saved count unchanged on failure so later saves can retry. +#if defined(_WIN32) || defined(_WIN64) + const bool saved = ::MoveFileExA(tmp_path.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING) != 0; +#else + const bool saved = std::rename(tmp_path.c_str(), path.c_str()) == 0; +#endif + if (saved) { + persistent_header_cache_saved_count() = source_cache.size(); + } } }