Skip to content

Refactor/#50 optimizations from pr 33 and others#81

Merged
GUVWAF merged 21 commits into
meshtastic:masterfrom
zandi:refactor/#50-optimizations-from-pr-33-and-others
May 15, 2026
Merged

Refactor/#50 optimizations from pr 33 and others#81
GUVWAF merged 21 commits into
meshtastic:masterfrom
zandi:refactor/#50-optimizations-from-pr-33-and-others

Conversation

@zandi
Copy link
Copy Markdown
Contributor

@zandi zandi commented May 7, 2026

Big-ish change incorporating some optimizations from the stale PR #33. At a high level this includes:

  • Adding a connectivity map optimization to restrict pathloss calculations to only feasibly reachable nodes
  • cache baseline pathloss when using the connectivity map to avoid computing it twice
  • completely refactor asymmetric link simulation to occur on each packet generation, rather than pre-computed in a lookup table of paths

As far as the optimizations go, they do seem worth it, but may have drawbacks for certain simulations. For example because the connectivity map must be updated on every node movement, we get an O(n) computation (n: # of nodes) for every node move. If there are lots of moving nodes this could get bad, but I haven't tested it rigorously.

I made sure the connectivity map optimization has no impact on simulation results, which is a bit tricky. Likewise I make sure the refactored asymmetric link simulation in combination with the connectivity map doesn't outrageously change simulation results, but this might give us a more limited range of signal strengths that are simulated so we don't generate a packet that would give inconsistent results with the connectivity map enabled.

Also add a lot of debug lines when I was working on an optimization to let us skip iterating over every node for every packet when collecting simulation results, but this was getting tricky and I wasn't sure the time savings would be worth it. Maybe pick back up on this later.

Here's some abbreviated test results from default config simulations of 30, 40, 50 and 60 nodes, on my laptop. I can post the full files if you want:

running default configuration discrete sim of 30 nodes, 25 times. connectivity map enabled: False
fastest time (in seconds): 30.714809584984323
average time (in seconds), ignoring 5 slowest outliers: 31.83447311474738

running default configuration discrete sim of 30 nodes, 25 times. connectivity map enabled: True
fastest time (in seconds): 30.224879839021014
average time (in seconds), ignoring 5 slowest outliers: 31.14236113289953
running default configuration discrete sim of 40 nodes, 25 times. connectivity map enabled: False
fastest time (in seconds): 67.66202248400077
average time (in seconds), ignoring 5 slowest outliers: 68.62156354159961

running default configuration discrete sim of 40 nodes, 25 times. connectivity map enabled: True
fastest time (in seconds): 64.62083907201304
average time (in seconds), ignoring 5 slowest outliers: 65.11369283699896
running default configuration discrete sim of 50 nodes, 25 times. connectivity map enabled: False
fastest time (in seconds): 112.10289663900039
average time (in seconds), ignoring 5 slowest outliers: 113.58745249010244

running default configuration discrete sim of 50 nodes, 25 times. connectivity map enabled: True
fastest time (in seconds): 101.47161630098708
average time (in seconds), ignoring 5 slowest outliers: 103.01543345100363
running default configuration discrete sim of 60 nodes, 25 times. connectivity map enabled: False
fastest time (in seconds): 139.81990426700213
average time (in seconds), ignoring 5 slowest outliers: 144.18966050099553

running default configuration discrete sim of 60 nodes, 25 times. connectivity map enabled: True
fastest time (in seconds): 134.20621736597968
average time (in seconds), ignoring 5 slowest outliers: 138.2929023791483

Just counting the fastest times, the optimizations reduce runtime for a default configuration sim by about 1.6% - 9.5%. More testing would be interesting to see if there's any sizes of a default config that wind up with this being slower.

I skipped the more drastic implementation from #33 that limited pathloss calculations to only 100 nodes, assuming that would certainly change simulation results. But I haven't tried a sim that large, and maybe there's an acceptable way to do that and get sim results that approximate a full sim without that shortcut.

zandi added 15 commits May 5, 2026 13:22
We just copy.deepcopy the config object so we operate on our own config.
setup_asymmetric_links sets up asymmetric links by adding a LINK_OFFSET
member to the config. Add a docstring for the function mentioning
this but otherwise leave it as-is.

Also add a test to make sure that running a DiscreteEventSim doesn't
change the config object we provide to create the sim.
The timing can vary quite a bit between runs depending on what else is
happening on the system, but this at least helps get some numbers when
working on optimizations. Hopefully we can make improvements on the big-O
runtime that will be easy to see even in rough numbers.
This uses a connectivity map to track what nodes could feasibly 'hear' each
other. It is symmetric and with ample margin to accomodate asymmetric link
behavior. A margin of 8 is chosen based on the 10-node 'gold standard' test
run, where one link has an offset of +7.5. Later reimplementation of asymmetric
link simulation can remove this link offset data structure and narrow this
margin further, which should improve performance slightly, but will necessarily
change our 'gold standard' reference test run.

The connectivity map is initialized before simulation start by a O(n^2) loop
that computes the set of reachable nodes from any arbitrary transmitting node.
This runtime could probably be reduced by 1/2 since links are symmetric, but
worry about that later (and it would still be O(n^2)).

This connectivity map must be updated every time a node moves, but this is
computed only from the perspective of the moving node, with some set operations
used to update the connectivity maps of other nodes as necessary. So this is
O(n), multiplied by every time a mobile node moves.

This connectivity map is used in MeshPacket to skip path loss/rssi/sensed
computations for nodes that aren't in the transmitting node's connectivity map.
This should skip computations for receiving nodes which can't even sense the
packet. This step is still O(n), but can skip potentially many unnecessary
computations. It all depends on node configuration.

Before this optimization, for path loss computations the O(n^2) precomputation
obviously doesn't happen, and neither would the O(n) computation for each node
move. However each packet creation has a O(n) computation of path loss that can
include potentially many unnecessary calculations, and there can be many more
packets in a simulation than nodes. Consider this all back-of-the-envelope
calculations rather than a rigorous analysis.

This gets the most benefit in situations with:
- dispersed nodes that can't reach many other nodes
- none or fewer moving nodes
- very many packets (long-running sim/more active network)
- and perhaps many nodes?

But may be worse for situations with:
- many moving nodes
- a densely connected network
- perhaps very many nodes (precomputation gets bad)

It is probably best to gate this behind an option w/ a config setting, so that
it can be enabled/disabled as appropriate for the scenario being simulated.

This can be further improved (perhaps greatly?) by caching path loss
calculations in the connectivity map, since we've already computed those and
can just re-use that value. Do this once asymmetric link simulation is moved
into the packet, rather than baked into the config using link offsets.
Since we're already computing the baseline path loss between nodes for our
connectivity matrix, just cache those values and reuse them later when
constructing packets. This at least saves us distance and path loss
calculations for each packet that gets created.
…ity map optimization. Default True.

This way we can easily enable/disable this optimization, which may be
beneficial or detrimental in different situations. This will make it easy to
add CLI arguments later to toggle this, and make it easy to evaluate the
optimization across a variety of scenarios. This could potentially let us
encode some heuristics for when to enable or disable the optimization based on
the scenario given.
… of test runs

We (hardcoded) throw away the 5 slowest test runs, so must have 6 or more
to not crash. Just gate this argument with a manual check/exception.
Just make sure our optimization isn't unintentionally creating different
simulations/results. Previous in-dev testing made sure of this, but now it's
encoded in a test case. Currently only checking second-order results since
comparing first-order results like the list of packets & nodes requires adding
more comparison functions which would be handy, but is more work and can be its
own thing.
…dle defaults for txZ, rxZ

accepting the model as a parameter gives us some nice flexibility,
and the new default handling makes it clearer we are getting defaults
from the config passed as a parameter and not the module-level config.
…n MeshNode

Just more data we track for later analysis. Not actually used
yet, just updated. Mostly because it's in abandoned PR meshtastic#33
Additionally add some explanatory comments and some TODOs. This all
comes from debugging the double-count issue fixed in meshtastic#65, and I felt
it was helpful to keep around for detailed debugging of packet flows
Ideally the unique packet counter would be part of a sim's state. For
now, add a static method so we can reset the counter, which should let
us have consistent start-from-0 unique packet sequence numbers between
simulation runs. Perhaps later refactor this counter to be part of sim
state so we don't have to manually remember to reset it.
Previously asymmetric links were simulated with static, pre-computed offsets to
each potential link. Instead, add a random offset to the path loss at each
potential receiver on MeshPacket initialization. This makes the asymmetric link
simulation dynamic to better simulate transient conditions like moving clutter
or other uncontrollable factors.

To keep the simulation consistent between the connectivity map optimization
being enabled/disabled, do 'useless' asym_rng calls so runs can be
deterministic and consistent across that optimization being enabled/disabled.

We tune the asymmetric standard deviation to seemingly be quite low so that all
RSSIs are still within the connectivity_map margin. Exceeding that margin can
lead to inconsistent simulations depending on the connectivity map optimization
being enabled/disabled. Log if this happens.

Always compute the connectivity map on simulation init and remove the call to
setup_asymmetric_links. The asymmetric link generation was already doing an
O(n^2) iteration of nodes so we don't save on complexity, and the functionality
we still want from setup_asymmetric_links (totalPairs/noLink) can be rolled
into initialize_connectivity_map.

Rip out everywhere we can find the now-obsolete symmetricLinks/asymmetricLinks
variables and whatever depends on them.

Finally, update our 'gold standard' test, since this necessarily changes the
results.
…iables to NodeConfig

This deduplicates crucial code for computing rssi & pathloss between nodes. We
put this in NodeConfig so we can use it in discrete sim setup before nodes are
created. This requires moving some extra variables into NodeConfig. Also add
the NodeConfig provided to create a MeshNode as a member variable so we can
access the computation method, and supply the rx node's config to it. Many
other member variables are already convenience bindings into this object, so
may as well bind to the object itself.

Add some docstrings along the way.
These convenience bindings are useful for not having `self.node_conf` repeated
all over, but more intentionally show we consider the node_conf a mutable
member variable by referencing `self.node_conf` rather than the `nodeConfig`
parameter. Later we may add a copy() so we can give the MeshNode its own node
config and not have it risk mutating some external one provided to the
constructor.
@GUVWAF GUVWAF self-requested a review May 11, 2026 17:51
zandi added 2 commits May 12, 2026 20:03
The way CW windows and delays are calculated must have changed, the simulation
doesn't seem to match current stable firmware. Update the constants and
computations necessary so we match current stable firmware. At time of writing,
tag v2.7.15.567b8ea is the most recent 'stable' release on the meshtastic
website.

This necessarily changes the simulation and thus the results, so update the
'gold standard' 10-node test to match.

Additionally, add and improve logging around CW selection.
@zandi
Copy link
Copy Markdown
Contributor Author

zandi commented May 13, 2026

I just updated the various CW-related computation functions to match firmware v2.7.15.567b8ea, the current stable release. For reference, here's how the basic results of the 'gold standard' 10-node sim have changed.

before:

Router Type: ROUTER_TYPE.MANAGED_FLOOD
Number of messages created: 174
Number of packets sent: 853 to 7677 potential receivers
Number of collisions: 301
Number of packets sensed: 2870
Number of packets received: 2567
Delay average (ms): 13030.81
Average Tx air utilization: 4.93 %
Percentage of packets that collided: 10.49 
Average percentage of nodes reached: 79.76 
Percentage of received packets containing new message: 48.66 
Number of packets dropped by delay/hop limit: 1173
No links: 55.56 %
Number of moving nodes: 4
Number of moving nodes w/ GPS: 1

After:

Router Type: ROUTER_TYPE.MANAGED_FLOOD
Number of messages created: 179
Number of packets sent: 789 to 7101 potential receivers
Number of collisions: 336
Number of packets sensed: 2722
Number of packets received: 2388
Delay average (ms): 3750.26
Average Tx air utilization: 4.57 %
Percentage of packets that collided: 12.34 
Average percentage of nodes reached: 75.85 
Percentage of received packets containing new message: 51.17 
Number of packets dropped by delay/hop limit: 1004
No links: 55.56 %
Number of moving nodes: 4
Number of moving nodes w/ GPS: 1

The changes seem small to me, except the average delay, which I bet comes from removing a * 1000 in get_current_slot_time, which didn't match firmware.

@GUVWAF
Copy link
Copy Markdown
Member

GUVWAF commented May 14, 2026

The changes seem small to me, except the average delay, which I bet comes from removing a * 1000 in get_current_slot_time, which didn't match firmware.

The * 1000 should be there, because in the firmware the bandwidth is stored in kHz, whereas in conf.py, MODEM_PRESETS stores the bandwidth in Hz.

Copy link
Copy Markdown
Member

@GUVWAF GUVWAF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice! Left a few comments still.

Comment thread utils/time-discrete-sim.py
Comment thread lib/packet.py Outdated
@zandi
Copy link
Copy Markdown
Contributor Author

zandi commented May 14, 2026 via email

@zandi
Copy link
Copy Markdown
Contributor Author

zandi commented May 14, 2026 via email

@GUVWAF
Copy link
Copy Markdown
Member

GUVWAF commented May 14, 2026

I could add it as a test that's skipped, or maybe make a utilities folder?

Yes, either of them would work for me!

zandi added 4 commits May 14, 2026 14:08
This directory can be for miscellaneous utilities that are worth
keeping around, but are meant to be used by developers or testers
rather than meshtasticator proper, or general users.
… knob.

Just cleaner. This call is to keep simulations using asymmetric links
consistent between having the connectivity map optimization on/off. It's
unnecessary to do this when not simulating asymmetric links, although doing so
doesn't change simulation behavior.
Add `now` to log lines, and change a debug log to a warning log, because it's
warning about a condition which will lead to inconsistent or unexpected
simulation results.
The firwmare stores bandwidth in KHz, but our config stores bandwidth in Hz.
So, scale it to convert from Hz to KHz so our calculation can match the
firmware.

This slightly changes the 'gold standard' 10-node unit test, so also update
that.
@zandi
Copy link
Copy Markdown
Contributor Author

zandi commented May 14, 2026

Let me know how that all looks. While correcting the bandwidth unit conversion in get_current_slot_time, I wanted to just change the config to be in KHz to match the firmware, and update everywhere that bandwidth is used. But I decided to not go down that rabbithole now, maybe that would make a good separate issue/beginner issue?

@GUVWAF
Copy link
Copy Markdown
Member

GUVWAF commented May 15, 2026

This looks good to me, thanks for the adjustments.

Yeah, maybe storing the bandwidth in kHz would be better to be similar to the firmware, but I'm fine with like it is now.

@GUVWAF GUVWAF merged commit fd8d0db into meshtastic:master May 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants