Skip to content

Commit cb41f67

Browse files
authored
Merge pull request erematorg#148 from M1thieu/main
feat(physics): entity-based conservation tracking and rotation (transitional)
2 parents 16a04c7 + e2d4baa commit cb41f67

9 files changed

Lines changed: 589 additions & 59 deletions

File tree

crates/energy/src/conservation.rs

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use bevy::prelude::*;
2+
use forces::prelude::{RotationalWorkEvent, WorkDoneEvent};
23

34
/// Enum representing different types of energy
45
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Component, Reflect)]
@@ -86,7 +87,7 @@ pub struct EnergyTransferEvent {
8687
/// Component for precise energy accounting
8788
#[derive(Component, Debug, Reflect)]
8889
#[reflect(Component)]
89-
pub struct EnergyAccountingLedger {
90+
pub struct EnergyBalance {
9091
/// History of all transactions, newest first
9192
pub transactions: Vec<EnergyTransaction>,
9293
/// Maximum number of transactions to store
@@ -116,7 +117,7 @@ pub struct EnergyTransaction {
116117
pub duration: f32,
117118
}
118119

119-
impl Default for EnergyAccountingLedger {
120+
impl Default for EnergyBalance {
120121
fn default() -> Self {
121122
Self {
122123
transactions: Vec::new(),
@@ -127,7 +128,7 @@ impl Default for EnergyAccountingLedger {
127128
}
128129
}
129130

130-
impl EnergyAccountingLedger {
131+
impl EnergyBalance {
131132
/// Record a new energy transaction
132133
pub fn record_transaction(&mut self, transaction: EnergyTransaction) {
133134
match transaction.transaction_type {
@@ -258,6 +259,72 @@ impl EnergyDriftMonitor {
258259
}
259260
}
260261

262+
/// System to ensure entities with Mass have energy balance tracking
263+
pub fn initialize_energy_balance(
264+
mut commands: Commands,
265+
query: Query<Entity, (With<forces::prelude::Mass>, Without<EnergyBalance>)>,
266+
) {
267+
for entity in query.iter() {
268+
commands.entity(entity).insert(EnergyBalance::default());
269+
}
270+
}
271+
272+
/// System to track work done by forces and record in energy balance
273+
pub fn track_work_from_forces(
274+
mut work_events: MessageReader<WorkDoneEvent>,
275+
mut query: Query<&mut EnergyBalance>,
276+
time: Res<Time>,
277+
) {
278+
for event in work_events.read() {
279+
if let Ok(mut ledger) = query.get_mut(event.entity) {
280+
// Correct transaction direction based on work sign
281+
let (transaction_type, source, destination) = if event.work > 0.0 {
282+
(TransactionType::Input, None, Some(event.entity))
283+
} else {
284+
(TransactionType::Output, Some(event.entity), None)
285+
};
286+
287+
ledger.record_transaction(EnergyTransaction {
288+
transaction_type,
289+
amount: event.work.abs(),
290+
source,
291+
destination,
292+
timestamp: time.elapsed_secs(),
293+
transfer_rate: event.work.abs() / time.delta_secs().max(f32::EPSILON),
294+
duration: time.delta_secs(),
295+
});
296+
}
297+
}
298+
}
299+
300+
/// System to track rotational work done by torques and record in energy balance
301+
pub fn track_rotational_work_from_torques(
302+
mut work_events: MessageReader<RotationalWorkEvent>,
303+
mut query: Query<&mut EnergyBalance>,
304+
time: Res<Time>,
305+
) {
306+
for event in work_events.read() {
307+
if let Ok(mut ledger) = query.get_mut(event.entity) {
308+
// Same logic as linear work: positive work = energy input, negative = output
309+
let (transaction_type, source, destination) = if event.work > 0.0 {
310+
(TransactionType::Input, None, Some(event.entity))
311+
} else {
312+
(TransactionType::Output, Some(event.entity), None)
313+
};
314+
315+
ledger.record_transaction(EnergyTransaction {
316+
transaction_type,
317+
amount: event.work.abs(),
318+
source,
319+
destination,
320+
timestamp: time.elapsed_secs(),
321+
transfer_rate: event.work.abs() / time.delta_secs().max(f32::EPSILON),
322+
duration: time.delta_secs(),
323+
});
324+
}
325+
}
326+
}
327+
261328
/// Plugin to manage energy conservation systems
262329
pub struct EnergyConservationPlugin;
263330

@@ -269,11 +336,22 @@ impl Plugin for EnergyConservationPlugin {
269336
.register_type::<EnergyQuantity>()
270337
.register_type::<TransactionType>()
271338
.register_type::<EnergyTransaction>()
272-
.register_type::<EnergyAccountingLedger>()
339+
.register_type::<EnergyBalance>()
273340
// Add resources
274341
.init_resource::<EnergyConservationTracker>()
275342
// Add event channel
276-
.add_message::<EnergyTransferEvent>();
343+
.add_message::<EnergyTransferEvent>()
344+
// Add systems with explicit ordering
345+
.add_systems(
346+
Update,
347+
(
348+
initialize_energy_balance,
349+
ApplyDeferred,
350+
(track_work_from_forces, track_rotational_work_from_torques)
351+
.after(forces::PhysicsSet::ApplyForces),
352+
)
353+
.chain(),
354+
);
277355
}
278356
}
279357

@@ -284,7 +362,7 @@ mod tests {
284362
#[test]
285363
fn test_ledger_arithmetic() {
286364
// Test that net_energy_change = total_input - total_output
287-
let mut ledger = EnergyAccountingLedger::default();
365+
let mut ledger = EnergyBalance::default();
288366

289367
ledger.record_transaction(EnergyTransaction {
290368
transaction_type: TransactionType::Input,
@@ -324,7 +402,7 @@ mod tests {
324402
#[test]
325403
fn test_current_flux_sums_active_rates() {
326404
// Test that current_flux sums transfer rates correctly
327-
let mut ledger = EnergyAccountingLedger::default();
405+
let mut ledger = EnergyBalance::default();
328406

329407
let current_time = 10.0;
330408

crates/energy/src/electromagnetism/charges.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,12 @@ pub fn apply_coulomb_pairwise_forces(
197197
continue;
198198
}
199199

200-
// Coulomb force: F = k·q₁·q₂/r²
200+
// Coulomb force: F_on_A = -k·q₁·q₂/r² · r̂_AB
201+
// Same-sign charges (k_qq > 0) → repulsive (force pushes A away from B)
202+
// Opposite-sign charges (k_qq < 0) → attractive (force pulls A toward B)
201203
let k_qq = config.coulomb_constant * charge_a * charge_b;
202204
let force_magnitude = k_qq / r.powi(2);
203-
let force_bare = (force_magnitude / r) * r_vec; // F_bare = (k·q₁·q₂/r²) · r̂
205+
let force_bare = -(force_magnitude / r) * r_vec;
204206

205207
// Apply C¹ force-switch for smooth cutoff
206208
let switch = force_switch(r, config.switch_on_radius, config.cutoff_radius);

crates/energy/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ pub mod prelude {
113113
pub use super::{EnergySystem, EnergyTransferError};
114114

115115
pub use crate::conservation::{
116-
EnergyAccountingLedger, EnergyConservationPlugin, EnergyConservationTracker,
117-
EnergyDriftMonitor, EnergyQuantity, EnergyTransaction, EnergyTransferEvent, EnergyType,
118-
TransactionType, conversion_efficiency, verify_conservation,
116+
EnergyBalance, EnergyConservationPlugin, EnergyConservationTracker, EnergyDriftMonitor,
117+
EnergyQuantity, EnergyTransaction, EnergyTransferEvent, EnergyType, TransactionType,
118+
conversion_efficiency, verify_conservation,
119119
};
120120

121121
pub use crate::electromagnetism::prelude::*;

crates/energy/src/thermodynamics/thermal.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
use bevy::prelude::*;
2-
use forces::PhysicsSet;
3-
use forces::core::newton_laws::Mass;
42
use matter::geometry::Radius;
53
use std::collections::HashMap;
64
use utils::{SpatialIndexSet, SpatiallyIndexed, UnifiedSpatialIndex, force_switch};
@@ -474,20 +472,22 @@ fn check_thermal_stability(
474472
/// **Conservation**: Thermal energy tracked, but not yet integrated with ledger.
475473
fn sync_thermal_energy(
476474
mut commands: Commands,
477-
changed_temps: Query<(Entity, &Temperature, &HeatCapacity, &Mass), Changed<Temperature>>,
475+
changed_temps: Query<(Entity, &Temperature, &HeatCapacity), Changed<Temperature>>,
478476
) {
479-
// **LP-0 SCAFFOLDING**: U = m·c_p·T (simplified thermal energy).
477+
// **LP-0 SCAFFOLDING**: U = C·T (simplified thermal energy).
478+
// HeatCapacity already includes mass: C = m·c_p (J/K).
480479
// Future: Use proper thermodynamic potentials (enthalpy for constant P, etc.).
481480

482-
for (entity, temp, heat_cap, mass) in changed_temps.iter() {
483-
// Thermal energy: U = m·c_p·T (Joules)
481+
for (entity, temp, heat_cap) in changed_temps.iter() {
482+
// Thermal energy: U = C·T (Joules)
483+
// where C = HeatCapacity = m·c_p (J/K), already accounts for mass.
484484
//
485485
// **APPROXIMATION**: Assumes constant c_p (valid for small ΔT).
486486
// Assumes T_ref = 0 K (absolute thermal energy).
487487
//
488488
// **IRL PHYSICS**: For phase changes, need enthalpy H = U + PV.
489489
// For proper thermodynamics, need U(S,V) or H(S,P).
490-
let thermal_energy = mass.value * heat_cap.value * temp.value;
490+
let thermal_energy = heat_cap.value * temp.value;
491491

492492
commands.entity(entity).insert(EnergyQuantity {
493493
value: thermal_energy,
@@ -515,8 +515,18 @@ impl Plugin for ThermalSystemPlugin {
515515
PreUpdate,
516516
mark_thermal_entities_spatially_indexed.in_set(SpatialIndexSet::InjectMarkers),
517517
)
518-
// Thermal transfer in Update - doesn't write AppliedForce, no conflict
519-
.add_systems(Update, sync_thermal_energy);
518+
// Thermal conduction → flush commands → sync energy.
519+
// conduction inserts Temperature via Commands; apply_deferred flushes
520+
// so sync_thermal_energy sees Changed<Temperature> in the same frame.
521+
.add_systems(
522+
Update,
523+
(
524+
compute_fourier_conduction,
525+
ApplyDeferred,
526+
sync_thermal_energy,
527+
)
528+
.chain(),
529+
);
520530
}
521531
}
522532

crates/energy/src/waves/propagation.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ pub(crate) struct WaveGrid(pub(crate) SpatialGrid);
66

77
use super::normalize_or;
88
use super::oscillation::{WaveParameters, angular_frequency, wave_number};
9-
use crate::conservation::{
10-
EnergyAccountingLedger, EnergyQuantity, EnergyTransaction, TransactionType,
11-
};
9+
use crate::conservation::{EnergyBalance, EnergyQuantity, EnergyTransaction, TransactionType};
1210

1311
// Calculate modified angular frequency with dispersion
1412
#[inline]
@@ -144,7 +142,7 @@ pub fn apply_wave_damping_with_energy(
144142
(Entity, &WaveParameters, Option<&mut EnergyQuantity>),
145143
With<WaveCenterMarker>,
146144
>,
147-
mut ledger_query: Query<&mut EnergyAccountingLedger>,
145+
mut ledger_query: Query<&mut EnergyBalance>,
148146
) {
149147
let dt = time.delta_secs();
150148
if dt == 0.0 {
@@ -179,8 +177,8 @@ pub fn apply_wave_damping_with_energy(
179177
}
180178
}
181179

182-
// Record transaction to ledger
183-
if let Ok(mut ledger) = ledger_query.single_mut() {
180+
// Record transaction to this entity's ledger
181+
if let Ok(mut ledger) = ledger_query.get_mut(entity) {
184182
ledger.record_transaction(EnergyTransaction {
185183
transaction_type: TransactionType::Output,
186184
amount: energy_lost,

crates/forces/src/core/gravity.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -437,15 +437,13 @@ pub fn calculate_barnes_hut_attraction(
437437

438438
affected_query
439439
.par_iter_mut()
440-
.for_each(|(entity, transform, _, mut force)| {
440+
.for_each(|(entity, transform, mass, mut force)| {
441441
let position = transform.translation;
442442

443-
if bodies.iter().any(|&(e, _, _)| e == entity) {
444-
return;
445-
}
446-
447443
let force_vector = calculate_barnes_hut_force(
444+
entity,
448445
position,
446+
mass.value,
449447
&quadtree.root,
450448
theta,
451449
softening,
@@ -457,7 +455,9 @@ pub fn calculate_barnes_hut_attraction(
457455
}
458456

459457
pub fn calculate_barnes_hut_force(
458+
affected_entity: Entity,
460459
affected_position: Vec3,
460+
affected_mass: f32,
461461
node: &spatial::QuadtreeNode,
462462
theta: f32,
463463
softening: f32,
@@ -470,7 +470,8 @@ pub fn calculate_barnes_hut_force(
470470
let distance_squared = direction.length_squared();
471471
let softened_distance_squared = distance_squared + softening_squared;
472472
let force_magnitude =
473-
gravitational_constant * node.mass_properties.total_mass / softened_distance_squared;
473+
gravitational_constant * affected_mass * node.mass_properties.total_mass
474+
/ softened_distance_squared;
474475

475476
if !force_magnitude.is_finite() {
476477
return Vec3::ZERO; // Skip non-finite BH aggregated force
@@ -482,7 +483,12 @@ pub fn calculate_barnes_hut_force(
482483
if node.children.iter().all(|c| c.is_none()) {
483484
let mut total_force = Vec3::ZERO;
484485

485-
for &(_, position, mass) in &node.bodies {
486+
for &(entity, position, mass) in &node.bodies {
487+
// Skip self-interaction
488+
if entity == affected_entity {
489+
continue;
490+
}
491+
486492
let direction = position - affected_position;
487493
let distance_squared = direction.length_squared();
488494

@@ -491,7 +497,8 @@ pub fn calculate_barnes_hut_force(
491497
}
492498

493499
let softened_distance_squared = distance_squared + softening_squared;
494-
let force_magnitude = gravitational_constant * mass / softened_distance_squared;
500+
let force_magnitude =
501+
gravitational_constant * affected_mass * mass / softened_distance_squared;
495502

496503
if !force_magnitude.is_finite() {
497504
continue; // Skip non-finite BH leaf force
@@ -506,7 +513,9 @@ pub fn calculate_barnes_hut_force(
506513
let mut total_force = Vec3::ZERO;
507514
for child_node in node.children.iter().flatten() {
508515
total_force += calculate_barnes_hut_force(
516+
affected_entity,
509517
affected_position,
518+
affected_mass,
510519
child_node,
511520
theta,
512521
softening,
@@ -746,9 +755,16 @@ mod tests {
746755
params.barnes_hut_max_bodies_per_node,
747756
);
748757

749-
for (i, _) in entities.iter().enumerate() {
750-
let bh_force =
751-
calculate_barnes_hut_force(bodies[i].0, &quadtree.root, 0.5, params.softening, g);
758+
for (i, &entity) in entities.iter().enumerate() {
759+
let bh_force = calculate_barnes_hut_force(
760+
entity,
761+
bodies[i].0,
762+
bodies[i].1,
763+
&quadtree.root,
764+
0.5,
765+
params.softening,
766+
g,
767+
);
752768
let diff = (bh_force - brute_forces[i]).length();
753769
assert!(diff < 1.0, "BH force differs from brute force by {}", diff);
754770
}

crates/forces/src/core/mod.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ pub mod prelude {
1515

1616
// Re-export from newton_laws module
1717
pub use crate::core::newton_laws::{
18-
AppliedForce, Distance, ForceImpulse, ForcesDiagnostics, ForcesDiagnosticsPlugin,
19-
IntegratorKind, Mass, NewtonLawsPlugin, Norm, PairedForce, PairedForceInteraction,
20-
Velocity, calculate_kinetic_energy, calculate_momentum, integrate_newton_second_law,
21-
integrate_positions_symplectic_euler, update_forces_diagnostics,
18+
AppliedForce, AppliedTorque, Distance, ForceImpulse, ForcesDiagnostics,
19+
ForcesDiagnosticsPlugin, IntegratorKind, Mass, MomentOfInertia, NewtonLawsPlugin, Norm,
20+
PairedForce, PairedForceInteraction, RotationalWorkEvent, Velocity, WorkDoneEvent,
21+
calculate_angular_momentum, calculate_kinetic_energy, calculate_momentum,
22+
calculate_rotational_kinetic_energy, calculate_torque_from_force,
23+
integrate_newton_second_law, integrate_positions_symplectic_euler, integrate_torques,
24+
update_forces_diagnostics,
2225
};
2326
}

0 commit comments

Comments
 (0)