Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 85 additions & 7 deletions crates/energy/src/conservation.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bevy::prelude::*;
use forces::prelude::{RotationalWorkEvent, WorkDoneEvent};

/// Enum representing different types of energy
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Component, Reflect)]
Expand Down Expand Up @@ -86,7 +87,7 @@ pub struct EnergyTransferEvent {
/// Component for precise energy accounting
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
pub struct EnergyAccountingLedger {
pub struct EnergyBalance {
/// History of all transactions, newest first
pub transactions: Vec<EnergyTransaction>,
/// Maximum number of transactions to store
Expand Down Expand Up @@ -116,7 +117,7 @@ pub struct EnergyTransaction {
pub duration: f32,
}

impl Default for EnergyAccountingLedger {
impl Default for EnergyBalance {
fn default() -> Self {
Self {
transactions: Vec::new(),
Expand All @@ -127,7 +128,7 @@ impl Default for EnergyAccountingLedger {
}
}

impl EnergyAccountingLedger {
impl EnergyBalance {
/// Record a new energy transaction
pub fn record_transaction(&mut self, transaction: EnergyTransaction) {
match transaction.transaction_type {
Expand Down Expand Up @@ -258,6 +259,72 @@ impl EnergyDriftMonitor {
}
}

/// System to ensure entities with Mass have energy balance tracking
pub fn initialize_energy_balance(
mut commands: Commands,
query: Query<Entity, (With<forces::prelude::Mass>, Without<EnergyBalance>)>,
) {
for entity in query.iter() {
commands.entity(entity).insert(EnergyBalance::default());
}
}

/// System to track work done by forces and record in energy balance
pub fn track_work_from_forces(
mut work_events: MessageReader<WorkDoneEvent>,
mut query: Query<&mut EnergyBalance>,
time: Res<Time>,
) {
for event in work_events.read() {
if let Ok(mut ledger) = query.get_mut(event.entity) {
// Correct transaction direction based on work sign
let (transaction_type, source, destination) = if event.work > 0.0 {
(TransactionType::Input, None, Some(event.entity))
} else {
(TransactionType::Output, Some(event.entity), None)
};

ledger.record_transaction(EnergyTransaction {
transaction_type,
amount: event.work.abs(),
source,
destination,
timestamp: time.elapsed_secs(),
transfer_rate: event.work.abs() / time.delta_secs().max(f32::EPSILON),
duration: time.delta_secs(),
});
}
}
}

/// System to track rotational work done by torques and record in energy balance
pub fn track_rotational_work_from_torques(
mut work_events: MessageReader<RotationalWorkEvent>,
mut query: Query<&mut EnergyBalance>,
time: Res<Time>,
) {
for event in work_events.read() {
if let Ok(mut ledger) = query.get_mut(event.entity) {
// Same logic as linear work: positive work = energy input, negative = output
let (transaction_type, source, destination) = if event.work > 0.0 {
(TransactionType::Input, None, Some(event.entity))
} else {
(TransactionType::Output, Some(event.entity), None)
};

ledger.record_transaction(EnergyTransaction {
transaction_type,
amount: event.work.abs(),
source,
destination,
timestamp: time.elapsed_secs(),
transfer_rate: event.work.abs() / time.delta_secs().max(f32::EPSILON),
duration: time.delta_secs(),
});
}
}
}

/// Plugin to manage energy conservation systems
pub struct EnergyConservationPlugin;

Expand All @@ -269,11 +336,22 @@ impl Plugin for EnergyConservationPlugin {
.register_type::<EnergyQuantity>()
.register_type::<TransactionType>()
.register_type::<EnergyTransaction>()
.register_type::<EnergyAccountingLedger>()
.register_type::<EnergyBalance>()
// Add resources
.init_resource::<EnergyConservationTracker>()
// Add event channel
.add_message::<EnergyTransferEvent>();
.add_message::<EnergyTransferEvent>()
// Add systems with explicit ordering
.add_systems(
Update,
(
initialize_energy_balance,
ApplyDeferred,
(track_work_from_forces, track_rotational_work_from_torques)
.after(forces::PhysicsSet::ApplyForces),
)
.chain(),
);
}
}

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

ledger.record_transaction(EnergyTransaction {
transaction_type: TransactionType::Input,
Expand Down Expand Up @@ -324,7 +402,7 @@ mod tests {
#[test]
fn test_current_flux_sums_active_rates() {
// Test that current_flux sums transfer rates correctly
let mut ledger = EnergyAccountingLedger::default();
let mut ledger = EnergyBalance::default();

let current_time = 10.0;

Expand Down
6 changes: 4 additions & 2 deletions crates/energy/src/electromagnetism/charges.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,12 @@ pub fn apply_coulomb_pairwise_forces(
continue;
}

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

// Apply C¹ force-switch for smooth cutoff
let switch = force_switch(r, config.switch_on_radius, config.cutoff_radius);
Expand Down
6 changes: 3 additions & 3 deletions crates/energy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ pub mod prelude {
pub use super::{EnergySystem, EnergyTransferError};

pub use crate::conservation::{
EnergyAccountingLedger, EnergyConservationPlugin, EnergyConservationTracker,
EnergyDriftMonitor, EnergyQuantity, EnergyTransaction, EnergyTransferEvent, EnergyType,
TransactionType, conversion_efficiency, verify_conservation,
EnergyBalance, EnergyConservationPlugin, EnergyConservationTracker, EnergyDriftMonitor,
EnergyQuantity, EnergyTransaction, EnergyTransferEvent, EnergyType, TransactionType,
conversion_efficiency, verify_conservation,
};

pub use crate::electromagnetism::prelude::*;
Expand Down
28 changes: 19 additions & 9 deletions crates/energy/src/thermodynamics/thermal.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
use bevy::prelude::*;
use forces::PhysicsSet;
use forces::core::newton_laws::Mass;
use matter::geometry::Radius;
use std::collections::HashMap;
use utils::{SpatialIndexSet, SpatiallyIndexed, UnifiedSpatialIndex, force_switch};
Expand Down Expand Up @@ -474,20 +472,22 @@ fn check_thermal_stability(
/// **Conservation**: Thermal energy tracked, but not yet integrated with ledger.
fn sync_thermal_energy(
mut commands: Commands,
changed_temps: Query<(Entity, &Temperature, &HeatCapacity, &Mass), Changed<Temperature>>,
changed_temps: Query<(Entity, &Temperature, &HeatCapacity), Changed<Temperature>>,
) {
// **LP-0 SCAFFOLDING**: U = m·c_p·T (simplified thermal energy).
// **LP-0 SCAFFOLDING**: U = C·T (simplified thermal energy).
// HeatCapacity already includes mass: C = m·c_p (J/K).
// Future: Use proper thermodynamic potentials (enthalpy for constant P, etc.).

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

commands.entity(entity).insert(EnergyQuantity {
value: thermal_energy,
Expand Down Expand Up @@ -515,8 +515,18 @@ impl Plugin for ThermalSystemPlugin {
PreUpdate,
mark_thermal_entities_spatially_indexed.in_set(SpatialIndexSet::InjectMarkers),
)
// Thermal transfer in Update - doesn't write AppliedForce, no conflict
.add_systems(Update, sync_thermal_energy);
// Thermal conduction → flush commands → sync energy.
// conduction inserts Temperature via Commands; apply_deferred flushes
// so sync_thermal_energy sees Changed<Temperature> in the same frame.
.add_systems(
Update,
(
compute_fourier_conduction,
ApplyDeferred,
sync_thermal_energy,
)
.chain(),
);
}
}

Expand Down
10 changes: 4 additions & 6 deletions crates/energy/src/waves/propagation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ pub(crate) struct WaveGrid(pub(crate) SpatialGrid);

use super::normalize_or;
use super::oscillation::{WaveParameters, angular_frequency, wave_number};
use crate::conservation::{
EnergyAccountingLedger, EnergyQuantity, EnergyTransaction, TransactionType,
};
use crate::conservation::{EnergyBalance, EnergyQuantity, EnergyTransaction, TransactionType};

// Calculate modified angular frequency with dispersion
#[inline]
Expand Down Expand Up @@ -144,7 +142,7 @@ pub fn apply_wave_damping_with_energy(
(Entity, &WaveParameters, Option<&mut EnergyQuantity>),
With<WaveCenterMarker>,
>,
mut ledger_query: Query<&mut EnergyAccountingLedger>,
mut ledger_query: Query<&mut EnergyBalance>,
) {
let dt = time.delta_secs();
if dt == 0.0 {
Expand Down Expand Up @@ -179,8 +177,8 @@ pub fn apply_wave_damping_with_energy(
}
}

// Record transaction to ledger
if let Ok(mut ledger) = ledger_query.single_mut() {
// Record transaction to this entity's ledger
if let Ok(mut ledger) = ledger_query.get_mut(entity) {
ledger.record_transaction(EnergyTransaction {
transaction_type: TransactionType::Output,
amount: energy_lost,
Expand Down
38 changes: 27 additions & 11 deletions crates/forces/src/core/gravity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,15 +437,13 @@ pub fn calculate_barnes_hut_attraction(

affected_query
.par_iter_mut()
.for_each(|(entity, transform, _, mut force)| {
.for_each(|(entity, transform, mass, mut force)| {
let position = transform.translation;

if bodies.iter().any(|&(e, _, _)| e == entity) {
return;
}

let force_vector = calculate_barnes_hut_force(
entity,
position,
mass.value,
&quadtree.root,
theta,
softening,
Expand All @@ -457,7 +455,9 @@ pub fn calculate_barnes_hut_attraction(
}

pub fn calculate_barnes_hut_force(
affected_entity: Entity,
affected_position: Vec3,
affected_mass: f32,
node: &spatial::QuadtreeNode,
theta: f32,
softening: f32,
Expand All @@ -470,7 +470,8 @@ pub fn calculate_barnes_hut_force(
let distance_squared = direction.length_squared();
let softened_distance_squared = distance_squared + softening_squared;
let force_magnitude =
gravitational_constant * node.mass_properties.total_mass / softened_distance_squared;
gravitational_constant * affected_mass * node.mass_properties.total_mass
/ softened_distance_squared;

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

for &(_, position, mass) in &node.bodies {
for &(entity, position, mass) in &node.bodies {
// Skip self-interaction
if entity == affected_entity {
continue;
}

let direction = position - affected_position;
let distance_squared = direction.length_squared();

Expand All @@ -491,7 +497,8 @@ pub fn calculate_barnes_hut_force(
}

let softened_distance_squared = distance_squared + softening_squared;
let force_magnitude = gravitational_constant * mass / softened_distance_squared;
let force_magnitude =
gravitational_constant * affected_mass * mass / softened_distance_squared;

if !force_magnitude.is_finite() {
continue; // Skip non-finite BH leaf force
Expand All @@ -506,7 +513,9 @@ pub fn calculate_barnes_hut_force(
let mut total_force = Vec3::ZERO;
for child_node in node.children.iter().flatten() {
total_force += calculate_barnes_hut_force(
affected_entity,
affected_position,
affected_mass,
child_node,
theta,
softening,
Expand Down Expand Up @@ -746,9 +755,16 @@ mod tests {
params.barnes_hut_max_bodies_per_node,
);

for (i, _) in entities.iter().enumerate() {
let bh_force =
calculate_barnes_hut_force(bodies[i].0, &quadtree.root, 0.5, params.softening, g);
for (i, &entity) in entities.iter().enumerate() {
let bh_force = calculate_barnes_hut_force(
entity,
bodies[i].0,
bodies[i].1,
&quadtree.root,
0.5,
params.softening,
g,
);
let diff = (bh_force - brute_forces[i]).length();
assert!(diff < 1.0, "BH force differs from brute force by {}", diff);
}
Expand Down
11 changes: 7 additions & 4 deletions crates/forces/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ pub mod prelude {

// Re-export from newton_laws module
pub use crate::core::newton_laws::{
AppliedForce, Distance, ForceImpulse, ForcesDiagnostics, ForcesDiagnosticsPlugin,
IntegratorKind, Mass, NewtonLawsPlugin, Norm, PairedForce, PairedForceInteraction,
Velocity, calculate_kinetic_energy, calculate_momentum, integrate_newton_second_law,
integrate_positions_symplectic_euler, update_forces_diagnostics,
AppliedForce, AppliedTorque, Distance, ForceImpulse, ForcesDiagnostics,
ForcesDiagnosticsPlugin, IntegratorKind, Mass, MomentOfInertia, NewtonLawsPlugin, Norm,
PairedForce, PairedForceInteraction, RotationalWorkEvent, Velocity, WorkDoneEvent,
calculate_angular_momentum, calculate_kinetic_energy, calculate_momentum,
calculate_rotational_kinetic_energy, calculate_torque_from_force,
integrate_newton_second_law, integrate_positions_symplectic_euler, integrate_torques,
update_forces_diagnostics,
};
}
Loading