Repository navigation
Releases
2.6.0
2.6.0 - Traces seed at grid indices, lie on the surface, and coast past the boundary
Follow Flow Path draws a traced arrow at chosen grid points or cells. Three
things made that harder than it had to be. The tracer took only positions, so
where a cell's glyph sits, and which seeds could trace, had to be worked out
outside this library, and a path could be paired with its index only by
matching positions. The positions a trace returned did not sit on the grid's
surface. And a trace that reached the edge of the grid stopped there, so arrows
near the boundary came out short or empty.
Seeding at grid indices
New: StartTracesAtIndices, and start_traces_at_indices in Python. It begins the
same batch StartTraces does, seeded at point or cell indices, with one entry per
index in the order given. A point seed starts at the point. A cell seed starts at
the mean of the cell's points, where a display drawing one glyph per cell puts
the glyph, rather than at its area centroid.
A seed traces only if it is active at both loaded time steps: a point when some
cell around it is active, a cell when it is itself. That is judged by the index,
not by where the seed lands, so a caller can predict it from its own activity.
A seed that does not trace keeps its slot, with an empty path,
GTEXIT_SEED_NOT_TRACEABLE and an XM_NODATA seed magnitude, so each result pairs
with its index by position alone.
The batch is refused whole when the location is neither points nor cells, the
times are not one per index, or an index is not a point or cell of the grid. In
Python that raises ValueError.
Every position lies on the grid's surface
Every position a trace returns on the grid, the seed's included, now takes its z
from the grid at its x and y, whatever z the seed was given. This holds for
TracePoint and StartTraces as well as the new call.
The z is interpolated with the weights of the lookup that found the velocity
there, so it costs no search of its own:
- A cell-located field is triangulated as each cell's centroid fan, which is the
triangulation a display draws the grid with. - A point-located field is triangulated between the grid's own points, so its z
comes from the points' elevations. Across a cell that is not planar, that can
differ from the fan a display draws.
Only the end of a path cut short by the max tracing distance takes a search,
since the field was never looked up there. It searches the field's own
triangulation.
Crossing the boundary no longer ends a trace
A trace that crossed the grid's boundary used to stop there with
GTEXIT_LEFT_GRID. Past the boundary there is no field to follow, so the trace now
carries on in a straight line at the velocity of the step that crossed, the
direction it left in. It keeps the z of the crossing, since there is no surface
to lay it on.
The coast ends like any other trace, on its time or distance budget. Otherwise it
waits at the end of the loaded window and resumes along the same line when the
next time step arrives. It does not follow the field again if its line passes
back over the grid.
The boundary includes the shores of holes in the grid, such as islands. A trace
that reaches an island coasts across it and on over the grid beyond, taking no
notice of the field there. This is deliberate, and the header says so.
An inactive cell is not the boundary. A trace still stops at its edge, with
GTEXIT_LEFT_GRID, even when the cell sits on the trace's way out of the grid.
That exit reason now means only that, and its message reads "Point has traveled
into an inactive cell." The name is unchanged so the enum's values and the
Python names stay stable, and the Python enum values now carry docstrings.
Where a step leaves the field is now found piece by piece between the edges it
crosses, not from its end alone. So a step that leaves across a gap and ends in a
dry cell beyond it coasts from where it left. A particle held still by a calm
field holds at the boundary rather than coasting off at the speed of
interpolation noise.
Every caller that traces out of the grid is affected: it gets back a path that
runs on past the boundary instead of one that ends at it.
Known limitations
- With no time or distance budget, a coast never ends. It runs to the end of
every window it is given, possibly far past the grid. A trace circling in the
field has the same property. Set a distance budget, as next_ms does for flow
paths. - A crossing is looked for only where a step ends with no field. So a hole or an
inactive cell narrower than a step can be stepped over. The max change distance
bounds how long a step can be.
Compatibility
StartTracesAtIndices is declared last in the header, after SetInitialDeltaTime,
so it appends a vtable slot rather than shifting the ones above it. It is a pure
virtual on a public abstract class, so an external subclass of XmGridTrace must
implement it. Callers using the New() factory are unaffected.
Results change in two ways:
- z: every position on the grid is on the surface.
- Boundary: traces that reach the boundary continue past it.
This is a minor release, as 2.5.0, 2.4.0 and 2.1.0 were for the same kinds of
change.
Dependencies are unchanged from 2.5.0: xmscore 7.0.13, xmsgrid 9.1.1,
xmsinterp 7.0.12, xmsextractor 10.0.10.
2.5.0
2.5.0 - The first step and the zero-velocity test scale to the field
Two constants tied the tracer to units the data never declares. A trace began
with a step of 1.0, and restarted with it; a velocity counted as zero below an
absolute .0001. On a time axis in days at ordinary speeds neither was
noticeable, which is why they lasted. Two cases were not so lucky: a uniformly
slow field, under 1e-4 everywhere, stopped every trace after one step; and a
field spinning up from rest on an axis in seconds was still "zero" one second
in, so the trace ended where it was released.
The first step is derived
It is the max change distance over the seed's speed, so the first step covers
at most that length whatever unit time is in. A still seed measures no speed of
its own - a field spinning up from rest is the usual case - so the fastest speed
in the loaded window stands in, which cannot overshoot whatever the seed picks
up during the step.
1.0 is kept as a fallback, for the two cases with nothing to derive a step from:
no max change distance, or a field still everywhere in both loaded steps.
New: GetInitialDeltaTime / SetInitialDeltaTime, and initial_delta_time in
Python, as a property and a constructor keyword. A positive value fixes the
first step. Zero or negative, the default, derives it. That is what a caller
pinned to the old numbers needs, and it is what the five tests carried forward
from before this change now set.
"Still" is relative
A speed counts as still when it is at most 1e-4 of the fastest speed either
loaded step reaches, after the vector multiplier. A uniformly slow field is
then no more still than a fast one scaled down, which is the point.
The peak counts only vectors interpolation can reach - the vectors of active
cells, or the points of active cells for point data. An inactive location can
hold anything, and one placeholder large enough to dominate would make every
real speed look still. A genuine outlier still sets it for the whole grid; the
header documents that trade-off rather than hiding it.
The distance cap's regularizer was max_change_distance * XM_ZERO_TOL, a length
added to a squared speed. It is the still speed squared now. The old term
outweighed the particle's own speed throughout a slow field and held every step
far below max_change_distance.
GTEXIT_ZERO_VELOCITY is terminal, but only when the field is still at both
loaded steps
Speed at a fixed point under linear time interpolation is the length of a vector
moving along a straight line, which is convex in time, so still at both ends
means still for the whole window. A calm that is only momentary holds the
particle instead: the step is accepted like any other, and the trace moves off
as the field picks up, or reaches the end of the window and waits for the next
time step. A cold start and a slack falling on a loaded step are both that case.
It stays terminal even though a later time step might move the particle again.
The alternative keeps every trace seeded in dead water alive, loading time
steps, for the rest of the series.
The test reuses the two steps' vectors from the lookup that found the field
still, so a hold costs no extra point-location searches. A cold start holds
every seed in a batch for its first several steps, so that mattered.
Known limitation
A reversal whose slack falls between two loaded steps is a sharp turn, and the
tracer halves the step toward it. With a small min delta time the trace crosses
the slack; with the default of 1 it usually stops with GTEXIT_MIN_DELTA_TIME, as
it did before this change. That default is the next constant carrying a unit the
data never declares, and it is left for its own release. A test pins both halves
of the behaviour so the change will be visible when it comes.
Compatibility
GetInitialDeltaTime and SetInitialDeltaTime are declared last in the header, so
they append vtable slots rather than shifting the ones above them. They are pure
virtuals on a public abstract class, so an external subclass of XmGridTrace must
implement them; callers using the New() factory are unaffected.
Results change only where max_change_distance / speed exceeds 1 - that is, in
slow fields. Everywhere else the distance cap already reduced the old 1.0 to the
same step, and a benchmark across a mixed field reproduced the previous search
counts, trace-point counts and exit reasons exactly once the first step was
pinned.
This is a minor release for the same reason 2.4.0 was, which added one pure
virtual to this interface, and 2.1.0, which added four.
Dependencies are unchanged from 2.4.0: xmscore 7.0.13, xmsgrid 9.1.1,
xmsinterp 7.0.12, xmsextractor 10.0.10.
2.4.0
2.4.0 - Sampling the field without tracing through it
One addition, and the behaviour around it. It completes what 2.3.0 started:
a client can now seed glyphs from a viewport lattice and draw them, whether
or not those glyphs go on to follow the flow.
New: SampleVectors / sample_vectors
Reports the field at each of a set of points that has one, without tracing
any of them. This is the interpolation a trace step already performs - one
point-location search per point against the cached triangulation, then the
two bracketing time steps blended at the requested time - exposed for
callers that want the field rather than a path through it.
The case it exists for is a display drawing a glyph per position on a moving
set of positions. ON_GRID placement anchors its lattice to the view and steps
it by the camera's parallel scale, so every pan and every zoom moves all of
them and the field has to be resampled wherever they land. (Rotation alone
does not: it moves neither the anchor nor the step.)
It reuses GetVectorAtLocationAndTime rather than sampling an extractor
alongside the tracer. If the two could disagree, a glyph and the path leaving
it would start from different fields.
Batched, for two reasons: the tracer's search scratch is reused across the
batch instead of reallocated per point, and a per-point call across a
language boundary costs more than the search it would perform.
Only the points that resolved are reported
Each comes back beside its own vector, so there is no no-data marker at the
API at all. A set of positions covering a view is mostly outside the grid or
over inactive cells; reporting those would hand every caller the same
filtering step, and one that forgot it would take XM_NODATA for a velocity.
Both outputs are subsets of the input's order, so a caller that does want to
know which of its points missed can walk the two in step. A miss in the
middle of a batch does not truncate it - the caller pairs glyphs to samples
by position, so a short batch would bind every later glyph to the wrong
sample.
The points kept are exactly the seeds StartTraces will accept, inactive cells
included. Both test the same GetVectorAtLocationAndTime result: StartTraces
to refuse a seed as GTEXIT_SEED_NOT_TRACEABLE, this to leave the point out.
A caller that follows the flow from these points needs no further filtering.
Two time steps are required, as tracing requires them. With fewer, nothing is
reported rather than a partial answer.
The sampled time is clamped to the loaded window
A time outside the two loaded steps reads the nearer one instead of
extrapolating a speed the field never had. That is also where a trace given
the same time comes to rest, so a glyph and the trace leaving it agree.
A non-finite result is rejected the same way a missing one is, because the
sentinel test alone does not catch one: EQ_TOL(NaN, XM_NODATA, 1) is false.
Two routes produce one, both from the caller - a NaN time passes both clamp
comparisons untouched, since NaN compares false against either bound, and two
steps loaded at the same time make the blend window zero, so its weights are
0/0. The guard tests the result rather than either cause, which covers both
and leaves the trace hot path unchanged.
Reports the field, not the tracing: the vector multiplier is not applied,
matching GetSeedMagnitudes. Every reported vector's z is zero, and the
reported points are the caller's own, echoed unchanged, z included - the
tracer is two-dimensional and never reads or writes a z velocity.
One tracer still serves one thread at a time, as everything else on it does.
The point-location search writes scratch held on the tracer, so concurrent
calls race on it even though this method is const.
Compatibility
SampleVectors is declared last in the header, so it appends a vtable slot
rather than shifting the ones above it. It is a pure virtual on a public
abstract class, so an external subclass of XmGridTrace must implement it;
callers using the New() factory are unaffected and need no changes.
This is a minor release for the same reason 2.3.0 was, which added one pure
virtual to this interface, and 2.1.0, which added four.
Dependencies are unchanged from 2.3.0: xmscore 7.0.13, xmsgrid 9.1.1,
xmsinterp 7.0.12, xmsextractor 10.0.10.
2.3.0
2.3.0 - Coexistence with xmsextractor, and seed speeds
Two changes, both needed before a client can seed glyphs from a viewport
lattice rather than from the grid's own points.
xmsgridtrace and xmsextractor can now be imported together
This module registered DataLocationEnum, which is xmsextractor's type, under
the same name xms.extractor registers it. pybind11's type registry is
process-global, so whichever module imported second raised
ImportError: generic_type: type "data_location_enum" is already registered!
The failure was symmetric - it did not matter which order you imported in -
so the two wheels were effectively mutually exclusive in one process, and
anything that traced could not also extract.
The registration is gone. Nothing here needed the export: this module's
location arguments are strings, converted to the enum as a local inside
add_grid_scalars_at_time, so no signature changed and no call site moves.
If you were reaching for the type itself off xms.gridtrace, it is at
xms.extractor._xmsextractor.extractor.data_location_enum. The extractor
package does not re-export it under its public name either, so that is the
only spelling available - there is no tidier one to offer.
New: GetSeedMagnitudes / get_seed_magnitudes
Reports the speed of the field at each seed, at the moment it was released.
A caller sizing a glyph by speed would otherwise interpolate the field a
second time; the tracer already interpolates at the seed to start the trace,
so asking it is free, where doing it outside costs another point-location
search per seed against the same triangulation.
It measures the field, not the tracing: the value is recorded before the
vector multiplier is applied. It is two-component - sqrt(vxvx + vyvy) -
because the tracer is two-dimensional and never reads a z velocity.
A seed the tracer never evaluated reports XM_NODATA, not 0.0. Zero is a legal
speed - a seed sitting in still water measures zero and exits
GTEXIT_ZERO_VELOCITY - so the two must not share a value, and XM_NODATA also
falls outside any magnitude filter rather than passing one with a minimum of
zero. Which unevaluated case it was (not started, waiting for a later time
step, not traceable, extraction failed) is in GetTraceResults' exit reasons.
Like GetTraceResults, this reports the batch: empty before StartTraces and
after a refused one, and untouched by TracePoint, which traces through its
own state.
Fixed: start_traces now discards a refused batch
XmGridTraceImpl::StartTraces clears the batch before validating, but the
Python binding raised on a points/times length mismatch before calling it.
A caller that caught the ValueError could then read the previous batch back
through get_trace_results as though it belonged to the new seeds - and zip
stale speeds against new points. This contradicted the binding's own
documented "starting a batch discards any previous one". It now refuses
through StartTraces first, then raises.
Compatibility
GetSeedMagnitudes is declared after GetExitMessage, so it appends a vtable
slot rather than shifting the ones above it. It is a pure virtual on a public
abstract class, so an external subclass of XmGridTrace must implement it;
callers using the New() factory are unaffected and need no changes.
This is a minor release for the same reason 2.1.0 was, which added four pure
virtuals to this interface.
Dependencies are unchanged from 2.2.0: xmscore 7.0.13, xmsgrid 9.1.1,
xmsinterp 7.0.12, xmsextractor 10.0.10.
2.2.0
2.2.0 - Python 3.14
Publishes xmsgridtrace for Python 3.14 alongside 3.13, and moves the pinned
dependency set to the releases that carry py3.14 binaries. No C++ source
changed in this release and the XmGridTrace API is unchanged - callers do not
need to re-baseline anything.
Wheels are now built for 3.14 on macOS, Linux and Windows. Windows continues
to publish 3.10 as well, for the desktop products.
Dependencies:
- xmscore 7.0.11 -> 7.0.13
- xmsgrid 9.0.10 -> 9.1.1
- xmsinterp 7.0.9 -> 7.0.12
- xmsextractor 10.0.7 -> 10.0.10
These are the minimum versions that publish py3.14 binaries, not simply the
latest. The previous pins publish only 3.10 and 3.13, so a 3.14 build would
resolve no binary at all; upstream rolled 3.14 out in a single wave on the
newest release of each, and the intermediate versions (xmscore 7.0.12,
xmsgrid 9.0.11 and 9.1.0, xmsinterp 7.0.11, xmsextractor 10.0.9) do not carry
it.
Note for anyone resolving xmsgridtrace alongside xmsgrid directly: the xmsgrid
pin crosses a minor version here, 9.0.x to 9.1.x. That is the reason this is a
minor release rather than a patch.
Build and CI:
- generated from xmsconan 2.23.0, up from 2.21.0
- third-party CI actions are pinned to commit SHAs rather than tags, so a
retargeted upstream tag cannot run in a job holding this repository secrets - the lint job now installs flake8-tidy-imports, which registers the
banned-modules option the generated .flake8 sets. Without the plugin flake8
accepted the osgeo ban and enforced nothing, while reporting the same green
as a run that had checked it - fanning macOS and Linux out across two ABIs renames their jobs and uploaded
artifacts to carry a -py suffix. Anything fetching release assets
or required status checks by exact name needs updating to match
2.1.0
2.1.0 - time-stepped tracing
Adds a resumable batch API to XmGridTrace so a trace can follow a vector
field through time instead of a single frozen instant. StartTraces /
ContinueTraces / GetTraceResults suspend each trace at the second loaded
time step and continue it when a later one is supplied, carrying position,
budgets, adaptive step size and previous velocity across the boundary. The
two-step window is retained, so memory stays bounded however long the
series is. Exit reasons are now an XmGridTraceExitEnum per trace rather
than a composed message string.
Corrections to existing behavior - callers relying on the old results
should re-baseline:
- the time interpolation was inverted; each step was weighted by its
distance from the other step, so at t == t1 the particle was advected
entirely by t2's field - no-data is propagated rather than blended, instead of averaging the
-9999999 sentinel into a real-looking velocity - TracePoint's position and time arrays could come back different lengths
- a null dereference when only one time step had been supplied
Performance, 200x200 quad grid at 10,000 seeds: a re-trace went from ~21 s
to ~150 ms, from caching the boundary-exit extractor instead of rebuilding
it per exit event, and one point-location search per triangulation instead
of four per sample. No xmsextractor change and no version cascade.
Also migrates the build to xmsconan 2.16.0, bumps xmsextractor to 10.0.6,
and removes AppVeyor.
2.0.3
Bump xmscore to 7.0.11, xmsgrid to 9.0.10, xmsinterp to 7.0.9, xmsextractor to 10.0.7, and xmsconan to ==2.16.0
- Flake job renders
.flake8via xmsconan and runs plainflake8 _package - Upgrade
actions/checkoutv2 -> v4; pinconan~=2.31.0
The previously pinned dependency versions are no longer usable. xmsconan 2.15.3 pinned conan~=2.31.0, changing package_id so older binaries no longer resolve; and xmsconan 2.15.1 moved testing_sources out of the published library, so libraries built between then and 2.16.0 do not export the testing helpers this library links. The versions pinned here are built under both.
Released from worktree-xmsconan-migration, not master — master does not yet carry the xmsconan migration.
Update documentation
Conda Fixes/Doc Fixes
Conda Review
- Rename Library
- Rename Python Library
- Rename Python Classes and Functions
- Update Docs