Skip to content

Implement allocation-based traffic economics and actor settlement#238

Open
wnj00524 wants to merge 1 commit into
masterfrom
codex/implement-economic-traffic-settlement-system
Open

Implement allocation-based traffic economics and actor settlement#238
wnj00524 wants to merge 1 commit into
masterfrom
codex/implement-economic-traffic-settlement-system

Conversation

@wnj00524
Copy link
Copy Markdown
Owner

@wnj00524 wnj00524 commented May 9, 2026

Motivation

  • Make agent BuyTraffic/SellTraffic semantics economically meaningful by ensuring revenue and profit are settled only for quantities actually delivered by RouteAllocation and by introducing explicit production costs and taxes.
  • Capture transport, production, and tax costs so profit follows: Profit = SaleRevenue - (TotalTransportCost + TotalProductionCost + TotalTax).
  • Preserve existing routing/allocation logic and quantities while attaching economic resolution to allocation results.

Description

  • Extended models: added DefaultUnitSalePrice, DefaultUnitProductionCost, SalesTaxRate, and RouteTaxRate to TrafficTypeDefinition; added ProductionCostPerUnit and SalesTaxRate to NodeTrafficProfile; added per-allocation economic fields to RouteAllocation and per-outcome totals to TrafficSimulationOutcome; extended EconomicSummary with sales/production/tax fields while keeping TotalRevenue populated for compatibility.
  • Added TrafficEconomicSettlementService and SimulationActorEconomicLedger to compute per-allocation SaleUnitPrice, SaleRevenue, TransportCostPerUnit, ProductionCostPerUnit, TotalTax, and Profit (using allocation.SourceUnitCostPerUnit as inherited input cost), resolve seller/buyer/tax authority, and produce actor ledgers and cash deltas.
  • Integrated settlement into the simulation pipeline by calling the settlement service after routing in NetworkSimulationEngine and applying ledger CashDelta updates to SimulationActorState.Cash in SimulationActorCoordinator, and exposed actor-level economic metrics in SimulationActorMetrics.
  • Implemented BuyTraffic/SellTraffic actions in SimulationActorActionApplier as intent updates that modify NodeTrafficProfile.Consumption/.Production (using AbsoluteValue or DeltaValue) without immediate cash transfer, and updated firm utility (MaximiseProfit) and EconomicCalculator to use settled seller-side economics and populate new summary fields.

Testing

  • Attempted to run unit tests with dotnet test tests/MedWNetworkSim.Tests/MedWNetworkSim.Tests.csproj, but the dotnet CLI is not available in this environment so automated tests could not be executed.
  • Changes were validated by code inspection and repository integration points were wired to preserve routing behavior and compatibility with existing public properties and JSON defaults.

Codex Task

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements an economic settlement layer for the network simulation, introducing the TrafficEconomicSettlementService to calculate revenues, production costs, and taxes. It also updates actor decision-making logic and simulation metrics to reflect these financial outcomes. The review feedback highlights a critical bug in the tax calculation logic that leads to a cash leak in the system. Furthermore, several performance improvements are suggested to address redundant settlement calls, inefficient actor lookups, and excessive object allocations during metrics generation.

Comment on lines +70 to +82
var profit = saleRevenue - (totalTransportCost + totalProductionCost + totalTax);

if (!string.IsNullOrWhiteSpace(sellerId))
{
var s = Ledger(sellerId!); s.SalesRevenue += saleRevenue; s.TransportCost += totalTransportCost; s.ProductionCost += totalProductionCost; s.TaxesPaid += totalTax; s.Profit += profit; s.CashDelta += profit;
}
if (!string.IsNullOrWhiteSpace(buyerId))
{
var b = Ledger(buyerId!); b.PurchaseCost += saleRevenue; b.TaxesPaid += totalTax; b.CashDelta -= (saleRevenue + totalTax);
}
if (!string.IsNullOrWhiteSpace(taxActorId))
{
var g = Ledger(taxActorId!); g.TaxesReceived += totalTax; g.CashDelta += totalTax;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The economic settlement logic for taxes is inconsistent and results in a cash leak from the simulation. Currently, totalTax is deducted from the buyer's cash (b.CashDelta -= (saleRevenue + totalTax)) and also deducted from the seller's profit (profit = saleRevenue - (... + totalTax)), which is then added to the seller's cash. However, the government only receives totalTax once (g.CashDelta += totalTax). This means totalTax is being subtracted twice from the total system cash but only added once, causing money to disappear from the simulation. Depending on the intended model, either the seller should receive the tax from the buyer before paying it to the government, or only one party should be responsible for the tax payment.

Comment on lines +43 to +48
string? ResolveActor(string nodeId)
{
var byControl = actorsById.Values.FirstOrDefault(a => a.ControlledNodeIds.Contains(nodeId, Comparer))?.Id;
if (!string.IsNullOrWhiteSpace(byControl)) return byControl;
return nodesById.TryGetValue(nodeId, out var node) ? node.ControllingActor : null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The ResolveActor method performs an $O(Actors \times Nodes)$ search for every allocation. In a network with many actors and allocations, this will become a significant performance bottleneck. Consider pre-calculating a mapping of NodeId to ActorId at the beginning of the Settle method to reduce this to an $O(1)$ lookup.

Comment on lines +135 to +139
TotalSalesRevenue = allocs.Sum(x => x.SaleRevenue),
TotalTransportCost = allocs.Sum(x => x.TotalTransportCost),
TotalProductionCost = allocs.Sum(x => x.TotalProductionCost),
TotalTax = allocs.Sum(x => x.TotalTax),
TotalProfit = allocs.Sum(x => x.Profit)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The code performs five separate Sum operations over the allocs list. This is inefficient as it iterates over the list multiple times. These totals should be accumulated in a single pass or during the initial allocation processing loop to improve performance.

Comment on lines +95 to +96
var settlement = new TrafficEconomicSettlementService().Settle(appliedNetwork, appliedSnapshot.TrafficOutcomes, actorMap);
appliedSnapshot = new VisualAnalyticsSnapshot { Network = appliedSnapshot.Network, TrafficOutcomes = settlement.Outcomes.ToList(), ConsumerCosts = appliedSnapshot.ConsumerCosts, Period = appliedSnapshot.Period };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There is a redundant call to TrafficEconomicSettlementService().Settle. The simulationEngine.Simulate method (called within BuildSnapshot on line 94) already performs economic settlement. However, since Simulate currently only returns the outcomes and not the actor ledgers, you are forced to call it again here. A better approach would be to refactor NetworkSimulationEngine.Simulate to return the full settlement result, avoiding the need to re-process all allocations and create duplicate objects.

Comment on lines +204 to +209
ActorSalesRevenueById = (ledgers ?? new Dictionary<string, SimulationActorEconomicLedger>(StringComparer.OrdinalIgnoreCase)).ToDictionary(k => k.Key, v => v.Value.SalesRevenue, StringComparer.OrdinalIgnoreCase),
ActorProductionCostById = (ledgers ?? new Dictionary<string, SimulationActorEconomicLedger>(StringComparer.OrdinalIgnoreCase)).ToDictionary(k => k.Key, v => v.Value.ProductionCost, StringComparer.OrdinalIgnoreCase),
ActorTransportCostById = (ledgers ?? new Dictionary<string, SimulationActorEconomicLedger>(StringComparer.OrdinalIgnoreCase)).ToDictionary(k => k.Key, v => v.Value.TransportCost, StringComparer.OrdinalIgnoreCase),
ActorTaxesPaidById = (ledgers ?? new Dictionary<string, SimulationActorEconomicLedger>(StringComparer.OrdinalIgnoreCase)).ToDictionary(k => k.Key, v => v.Value.TaxesPaid, StringComparer.OrdinalIgnoreCase),
ActorTaxesReceivedById = (ledgers ?? new Dictionary<string, SimulationActorEconomicLedger>(StringComparer.OrdinalIgnoreCase)).ToDictionary(k => k.Key, v => v.Value.TaxesReceived, StringComparer.OrdinalIgnoreCase),
ActorProfitById = (ledgers ?? new Dictionary<string, SimulationActorEconomicLedger>(StringComparer.OrdinalIgnoreCase)).ToDictionary(k => k.Key, v => v.Value.Profit, StringComparer.OrdinalIgnoreCase),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The BuildMetrics method creates six new dictionaries using ToDictionary on every call. Additionally, if ledgers is null, it instantiates a new empty dictionary six times. This is inefficient and creates unnecessary garbage collection pressure. Consider extracting a safeLedgers variable or using a more efficient way to populate these metrics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant