adding ai_economist for modding
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2020, salesforce.com, inc.
|
||||
# All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# For full license text, see the LICENSE file in the repo root
|
||||
# or https://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
from ai_economist.foundation.base.base_env import scenario_registry
|
||||
|
||||
from .covid19 import covid19_env
|
||||
from .one_step_economy import one_step_economy
|
||||
from .simple_wood_and_stone import dynamic_layout, layout_from_file
|
||||
|
||||
# Import files that add Scenario class(es) to scenario_registry
|
||||
# -------------------------------------------------------------
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) 2020, salesforce.com, inc.
|
||||
# All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# For full license text, see the LICENSE file in the repo root
|
||||
# or https://opensource.org/licenses/BSD-3-Clause
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2021, salesforce.com, inc.
|
||||
// All rights reserved.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// For full license text, see the LICENSE file in the repo root
|
||||
// or https://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
#ifndef CUDA_INCLUDES_COVID19_CONST_H_
|
||||
#define CUDA_INCLUDES_COVID19_CONST_H_
|
||||
|
||||
#include "../../components/covid19_components_step.cu"
|
||||
#include "covid19_env_step.cu"
|
||||
|
||||
#endif // CUDA_INCLUDES_COVID19_CONST_H_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,620 @@
|
||||
// Copyright (c) 2021, salesforce.com, inc.
|
||||
// All rights reserved.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// For full license text, see the LICENSE file in the repo root
|
||||
// or https://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
__constant__ float kEpsilon = 1.0e-10; // used to prevent division by 0
|
||||
|
||||
extern "C" {
|
||||
// CUDA version of the scenario_step() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
|
||||
// CUDA version of the sir_step() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
__device__ void cuda_sir_step(
|
||||
float* susceptible,
|
||||
float* infected,
|
||||
float* recovered,
|
||||
float* vaccinated,
|
||||
float* deaths,
|
||||
int* num_vaccines_available_t,
|
||||
const int* kRealWorldStringencyPolicyHistory,
|
||||
const float kStatePopulation,
|
||||
const int kNumAgents,
|
||||
const int kBetaDelay,
|
||||
const float kBetaSlope,
|
||||
const float kbetaIntercept,
|
||||
int* stringency_level,
|
||||
float* beta,
|
||||
const float kGamma,
|
||||
const float kDeathRate,
|
||||
const int kEnvId,
|
||||
const int kAgentId,
|
||||
int timestep,
|
||||
const int kEpisodeLength,
|
||||
const int kArrayIdxCurrentTime,
|
||||
const int kArrayIdxPrevTime,
|
||||
const int kTimeIndependentArrayIdx
|
||||
) {
|
||||
float susceptible_fraction_vaccinated = min(
|
||||
1.0,
|
||||
num_vaccines_available_t[kTimeIndependentArrayIdx] /
|
||||
(susceptible[kArrayIdxPrevTime] + kEpsilon));
|
||||
float vaccinated_t = min(
|
||||
static_cast<float>(num_vaccines_available_t[
|
||||
kTimeIndependentArrayIdx]),
|
||||
susceptible[kArrayIdxPrevTime]);
|
||||
|
||||
// (S/N) * I in place of (S*I) / N to prevent overflow
|
||||
float neighborhood_SI_over_N = susceptible[kArrayIdxPrevTime] /
|
||||
kStatePopulation * infected[kArrayIdxPrevTime];
|
||||
int stringency_level_tmk;
|
||||
if (timestep < kBetaDelay) {
|
||||
stringency_level_tmk = kRealWorldStringencyPolicyHistory[
|
||||
(timestep - 1) * (kNumAgents - 1) + kAgentId];
|
||||
} else {
|
||||
stringency_level_tmk = stringency_level[kEnvId * (
|
||||
kEpisodeLength + 1) * (kNumAgents - 1) +
|
||||
(timestep - kBetaDelay) * (kNumAgents - 1) + kAgentId];
|
||||
}
|
||||
beta[kTimeIndependentArrayIdx] = stringency_level_tmk *
|
||||
kBetaSlope + kbetaIntercept;
|
||||
|
||||
float dS_t = -(neighborhood_SI_over_N * beta[
|
||||
kTimeIndependentArrayIdx] *
|
||||
(1 - susceptible_fraction_vaccinated) + vaccinated_t);
|
||||
float dR_t = kGamma * infected[kArrayIdxPrevTime] + vaccinated_t;
|
||||
float dI_t = - dS_t - dR_t;
|
||||
|
||||
susceptible[kArrayIdxCurrentTime] = max(
|
||||
0.0,
|
||||
susceptible[kArrayIdxPrevTime] + dS_t);
|
||||
infected[kArrayIdxCurrentTime] = max(
|
||||
0.0,
|
||||
infected[kArrayIdxPrevTime] + dI_t);
|
||||
recovered[kArrayIdxCurrentTime] = max(
|
||||
0.0,
|
||||
recovered[kArrayIdxPrevTime] + dR_t);
|
||||
|
||||
vaccinated[kArrayIdxCurrentTime] = vaccinated_t +
|
||||
vaccinated[kArrayIdxPrevTime];
|
||||
float recovered_but_not_vaccinated = recovered[kArrayIdxCurrentTime] -
|
||||
vaccinated[kArrayIdxCurrentTime];
|
||||
deaths[kArrayIdxCurrentTime] = recovered_but_not_vaccinated *
|
||||
kDeathRate;
|
||||
}
|
||||
|
||||
// CUDA version of the softplus() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
__device__ float softplus(float x) {
|
||||
const float kBeta = 1.0;
|
||||
const float kThreshold = 20.0;
|
||||
if (kBeta * x < kThreshold) {
|
||||
return 1.0 / kBeta * log(1.0 + exp(kBeta * x));
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
__device__ float signal2unemployment(
|
||||
const int kEnvId,
|
||||
const int kAgentId,
|
||||
float* signal,
|
||||
const float* kUnemploymentConvolutionalFilters,
|
||||
const float kUnemploymentBias,
|
||||
const int kNumAgents,
|
||||
const int kFilterLen,
|
||||
const int kNumFilters
|
||||
) {
|
||||
float unemployment = 0.0;
|
||||
const int kArrayIndexOffset = kEnvId * (kNumAgents - 1) * kNumFilters *
|
||||
kFilterLen + kAgentId * kNumFilters * kFilterLen;
|
||||
for (int index = 0; index < (kFilterLen * kNumFilters); index ++) {
|
||||
unemployment += signal[kArrayIndexOffset + index] *
|
||||
kUnemploymentConvolutionalFilters[index];
|
||||
}
|
||||
return softplus(unemployment) + kUnemploymentBias;
|
||||
}
|
||||
|
||||
// CUDA version of the unemployment_step() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
__device__ void cuda_unemployment_step(
|
||||
float* unemployed,
|
||||
int* stringency_level,
|
||||
int* delta_stringency_level,
|
||||
const float* kGroupedConvolutionalFilterWeights,
|
||||
const float* kUnemploymentConvolutionalFilters,
|
||||
const float* kUnemploymentBias,
|
||||
float* convolved_signal,
|
||||
const int kFilterLen,
|
||||
const int kNumFilters,
|
||||
const float kStatePopulation,
|
||||
const int kNumAgents,
|
||||
const int kEnvId,
|
||||
const int kAgentId,
|
||||
int timestep,
|
||||
const int kArrayIdxCurrentTime,
|
||||
const int kArrayIdxPrevTime
|
||||
) {
|
||||
// Shift array by kNumAgents - 1
|
||||
for (int idx = 0; idx < kFilterLen - 1; idx ++) {
|
||||
delta_stringency_level[
|
||||
kEnvId * kFilterLen * (kNumAgents - 1) + idx *
|
||||
(kNumAgents - 1) + kAgentId
|
||||
] =
|
||||
delta_stringency_level[
|
||||
kEnvId * kFilterLen * (kNumAgents - 1) + (idx + 1) *
|
||||
(kNumAgents - 1) + kAgentId
|
||||
];
|
||||
}
|
||||
|
||||
delta_stringency_level[
|
||||
kEnvId * kFilterLen * (kNumAgents - 1) + (kFilterLen - 1) *
|
||||
(kNumAgents - 1) + kAgentId
|
||||
] = stringency_level[kArrayIdxCurrentTime] -
|
||||
stringency_level[kArrayIdxPrevTime];
|
||||
|
||||
// convolved_signal refers to the convolution between the filter weights
|
||||
// and the delta stringency levels
|
||||
for (int filter_idx = 0; filter_idx < kNumFilters; filter_idx ++) {
|
||||
for (int idx = 0; idx < kFilterLen; idx ++) {
|
||||
convolved_signal[
|
||||
kEnvId * (kNumAgents - 1) * kNumFilters * kFilterLen +
|
||||
kAgentId * kNumFilters * kFilterLen +
|
||||
filter_idx * kFilterLen +
|
||||
idx
|
||||
] =
|
||||
delta_stringency_level[kEnvId * kFilterLen * (kNumAgents - 1) +
|
||||
idx * (kNumAgents - 1) + kAgentId] *
|
||||
kGroupedConvolutionalFilterWeights[kAgentId * kNumFilters +
|
||||
filter_idx];
|
||||
}
|
||||
}
|
||||
|
||||
float unemployment_rate = signal2unemployment(
|
||||
kEnvId,
|
||||
kAgentId,
|
||||
convolved_signal,
|
||||
kUnemploymentConvolutionalFilters,
|
||||
kUnemploymentBias[kAgentId],
|
||||
kNumAgents,
|
||||
kFilterLen,
|
||||
kNumFilters);
|
||||
|
||||
unemployed[kArrayIdxCurrentTime] =
|
||||
unemployment_rate * kStatePopulation / 100.0;
|
||||
}
|
||||
|
||||
// CUDA version of the economy_step() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
__device__ void cuda_economy_step(
|
||||
float* infected,
|
||||
float* deaths,
|
||||
float* unemployed,
|
||||
float* incapacitated,
|
||||
float* cant_work,
|
||||
float* num_people_that_can_work,
|
||||
const float kStatePopulation,
|
||||
const float kInfectionTooSickToWorkRate,
|
||||
const float kPopulationBetweenAge18And65,
|
||||
const float kDailyProductionPerWorker,
|
||||
float* productivity,
|
||||
float* subsidy,
|
||||
float* postsubsidy_productivity,
|
||||
int timestep,
|
||||
const int kArrayIdxCurrentTime,
|
||||
int kTimeIndependentArrayIdx
|
||||
) {
|
||||
incapacitated[kTimeIndependentArrayIdx] =
|
||||
kInfectionTooSickToWorkRate * infected[kArrayIdxCurrentTime] +
|
||||
deaths[kArrayIdxCurrentTime];
|
||||
cant_work[kTimeIndependentArrayIdx] =
|
||||
incapacitated[kTimeIndependentArrayIdx] *
|
||||
kPopulationBetweenAge18And65 + unemployed[kArrayIdxCurrentTime];
|
||||
int num_workers = static_cast<int>(kStatePopulation) * kPopulationBetweenAge18And65;
|
||||
num_people_that_can_work[kTimeIndependentArrayIdx] = max(
|
||||
0.0,
|
||||
num_workers - cant_work[kTimeIndependentArrayIdx]);
|
||||
productivity[kArrayIdxCurrentTime] =
|
||||
num_people_that_can_work[kTimeIndependentArrayIdx] *
|
||||
kDailyProductionPerWorker;
|
||||
|
||||
postsubsidy_productivity[kArrayIdxCurrentTime] =
|
||||
productivity[kArrayIdxCurrentTime] +
|
||||
subsidy[kArrayIdxCurrentTime];
|
||||
}
|
||||
|
||||
// CUDA version of crra_nonlinearity() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
__device__ float crra_nonlinearity(
|
||||
float x,
|
||||
const float kEta,
|
||||
const int kNumDaysInAnYear
|
||||
) {
|
||||
float annual_x = kNumDaysInAnYear * x;
|
||||
float annual_x_clipped = annual_x;
|
||||
if (annual_x < 0.1) {
|
||||
annual_x_clipped = 0.1;
|
||||
} else if (annual_x > 3.0) {
|
||||
annual_x_clipped = 3.0;
|
||||
}
|
||||
float annual_crra = 1 + (pow(annual_x_clipped, (1 - kEta)) - 1) /
|
||||
(1 - kEta);
|
||||
float daily_crra = annual_crra / kNumDaysInAnYear;
|
||||
return daily_crra;
|
||||
}
|
||||
|
||||
// CUDA version of min_max_normalization() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
__device__ float min_max_normalization(
|
||||
float x,
|
||||
const float kMinX,
|
||||
const float kMaxX
|
||||
) {
|
||||
return (x - kMinX) / (kMaxX - kMinX + kEpsilon);
|
||||
}
|
||||
|
||||
// CUDA version of get_rew() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
__device__ float get_rew(
|
||||
const float kHealthIndexWeightage,
|
||||
float health_index,
|
||||
const float kEconomicIndexWeightage,
|
||||
float economic_index
|
||||
) {
|
||||
return (
|
||||
kHealthIndexWeightage * health_index
|
||||
+ kEconomicIndexWeightage * economic_index) /
|
||||
(kHealthIndexWeightage + kEconomicIndexWeightage);
|
||||
}
|
||||
|
||||
// CUDA version of scenario_step() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
__global__ void CudaCovidAndEconomySimulationStep(
|
||||
float* susceptible,
|
||||
float* infected,
|
||||
float* recovered,
|
||||
float* deaths,
|
||||
float* vaccinated,
|
||||
float* unemployed,
|
||||
float* subsidy,
|
||||
float* productivity,
|
||||
int* stringency_level,
|
||||
const int kNumStringencyLevels,
|
||||
float* postsubsidy_productivity,
|
||||
int* num_vaccines_available_t,
|
||||
const int* kRealWorldStringencyPolicyHistory,
|
||||
const int kBetaDelay,
|
||||
const float* kBetaSlopes,
|
||||
const float* kbetaIntercepts,
|
||||
float* beta,
|
||||
const float kGamma,
|
||||
const float kDeathRate,
|
||||
float* incapacitated,
|
||||
float* cant_work,
|
||||
float* num_people_that_can_work,
|
||||
const int* us_kStatePopulation,
|
||||
const float kInfectionTooSickToWorkRate,
|
||||
const float kPopulationBetweenAge18And65,
|
||||
const int kFilterLen,
|
||||
const int kNumFilters,
|
||||
int* delta_stringency_level,
|
||||
const float* kGroupedConvolutionalFilterWeights,
|
||||
const float* kUnemploymentConvolutionalFilters,
|
||||
const float* kUnemploymentBias,
|
||||
float* signal,
|
||||
const float kDailyProductionPerWorker,
|
||||
const float* maximum_productivity,
|
||||
float* obs_a_world_agent_state,
|
||||
float* obs_a_world_agent_postsubsidy_productivity,
|
||||
float* obs_a_world_lagged_stringency_level,
|
||||
float* obs_a_time,
|
||||
float* obs_p_world_agent_state,
|
||||
float* obs_p_world_agent_postsubsidy_productivity,
|
||||
float* obs_p_world_lagged_stringency_level,
|
||||
float* obs_p_time,
|
||||
int * env_timestep_arr,
|
||||
const int kNumAgents,
|
||||
const int kEpisodeLength
|
||||
) {
|
||||
const int kEnvId = blockIdx.x;
|
||||
const int kAgentId = threadIdx.x;
|
||||
|
||||
assert(env_timestep_arr[kEnvId] > 0 &&
|
||||
env_timestep_arr[kEnvId] <= kEpisodeLength);
|
||||
assert (kAgentId <= kNumAgents - 1);
|
||||
const int kNumFeatures = 6;
|
||||
|
||||
if (kAgentId < (kNumAgents - 1)) {
|
||||
// Indices for time-dependent and time-independent arrays
|
||||
// Time dependent arrays have shapes (num_envs,
|
||||
// kEpisodeLength + 1, kNumAgents - 1)
|
||||
// Time independent arrays have shapes (num_envs, kNumAgents - 1)
|
||||
const int kArrayIndexOffset = kEnvId * (kEpisodeLength + 1) *
|
||||
(kNumAgents - 1);
|
||||
int kArrayIdxCurrentTime = kArrayIndexOffset +
|
||||
env_timestep_arr[kEnvId] * (kNumAgents - 1) + kAgentId;
|
||||
int kArrayIdxPrevTime = kArrayIndexOffset +
|
||||
(env_timestep_arr[kEnvId] - 1) * (kNumAgents - 1) + kAgentId;
|
||||
const int kTimeIndependentArrayIdx = kEnvId *
|
||||
(kNumAgents - 1) + kAgentId;
|
||||
|
||||
const float kStatePopulation = static_cast<float>(us_kStatePopulation[kAgentId]);
|
||||
|
||||
cuda_sir_step(
|
||||
susceptible,
|
||||
infected,
|
||||
recovered,
|
||||
vaccinated,
|
||||
deaths,
|
||||
num_vaccines_available_t,
|
||||
kRealWorldStringencyPolicyHistory,
|
||||
kStatePopulation,
|
||||
kNumAgents,
|
||||
kBetaDelay,
|
||||
kBetaSlopes[kAgentId],
|
||||
kbetaIntercepts[kAgentId],
|
||||
stringency_level,
|
||||
beta,
|
||||
kGamma,
|
||||
kDeathRate,
|
||||
kEnvId,
|
||||
kAgentId,
|
||||
env_timestep_arr[kEnvId],
|
||||
kEpisodeLength,
|
||||
kArrayIdxCurrentTime,
|
||||
kArrayIdxPrevTime,
|
||||
kTimeIndependentArrayIdx);
|
||||
|
||||
cuda_unemployment_step(
|
||||
unemployed,
|
||||
stringency_level,
|
||||
delta_stringency_level,
|
||||
kGroupedConvolutionalFilterWeights,
|
||||
kUnemploymentConvolutionalFilters,
|
||||
kUnemploymentBias,
|
||||
signal,
|
||||
kFilterLen,
|
||||
kNumFilters,
|
||||
kStatePopulation,
|
||||
kNumAgents,
|
||||
kEnvId,
|
||||
kAgentId,
|
||||
env_timestep_arr[kEnvId],
|
||||
kArrayIdxCurrentTime,
|
||||
kArrayIdxPrevTime);
|
||||
|
||||
cuda_economy_step(
|
||||
infected,
|
||||
deaths,
|
||||
unemployed,
|
||||
incapacitated,
|
||||
cant_work,
|
||||
num_people_that_can_work,
|
||||
kStatePopulation,
|
||||
kInfectionTooSickToWorkRate,
|
||||
kPopulationBetweenAge18And65,
|
||||
kDailyProductionPerWorker,
|
||||
productivity,
|
||||
subsidy,
|
||||
postsubsidy_productivity,
|
||||
env_timestep_arr[kEnvId],
|
||||
kArrayIdxCurrentTime,
|
||||
kTimeIndependentArrayIdx);
|
||||
|
||||
// CUDA version of generate observations
|
||||
// Agents' observations
|
||||
int kFeatureArrayIndexOffset = kEnvId * kNumFeatures *
|
||||
(kNumAgents - 1) + kAgentId;
|
||||
obs_a_world_agent_state[
|
||||
kFeatureArrayIndexOffset + 0 * (kNumAgents - 1)
|
||||
] = susceptible[kArrayIdxCurrentTime] / kStatePopulation;
|
||||
obs_a_world_agent_state[
|
||||
kFeatureArrayIndexOffset + 1 * (kNumAgents - 1)
|
||||
] = infected[kArrayIdxCurrentTime] / kStatePopulation;
|
||||
obs_a_world_agent_state[
|
||||
kFeatureArrayIndexOffset + 2 * (kNumAgents - 1)
|
||||
] = recovered[kArrayIdxCurrentTime] / kStatePopulation;
|
||||
obs_a_world_agent_state[
|
||||
kFeatureArrayIndexOffset + 3 * (kNumAgents - 1)
|
||||
] = deaths[kArrayIdxCurrentTime] / kStatePopulation;
|
||||
obs_a_world_agent_state[
|
||||
kFeatureArrayIndexOffset + 4 * (kNumAgents - 1)
|
||||
] = vaccinated[kArrayIdxCurrentTime] / kStatePopulation;
|
||||
obs_a_world_agent_state[
|
||||
kFeatureArrayIndexOffset + 5 * (kNumAgents - 1)
|
||||
] = unemployed[kArrayIdxCurrentTime] / kStatePopulation;
|
||||
|
||||
for (int feature_id = 0; feature_id < kNumFeatures; feature_id ++) {
|
||||
const int kIndex = feature_id * (kNumAgents - 1);
|
||||
obs_p_world_agent_state[kFeatureArrayIndexOffset +
|
||||
kIndex
|
||||
] = obs_a_world_agent_state[kFeatureArrayIndexOffset +
|
||||
kIndex];
|
||||
}
|
||||
|
||||
obs_a_world_agent_postsubsidy_productivity[
|
||||
kTimeIndependentArrayIdx
|
||||
] = postsubsidy_productivity[kArrayIdxCurrentTime] /
|
||||
maximum_productivity[kAgentId];
|
||||
obs_p_world_agent_postsubsidy_productivity[
|
||||
kTimeIndependentArrayIdx
|
||||
] = obs_a_world_agent_postsubsidy_productivity[
|
||||
kTimeIndependentArrayIdx
|
||||
];
|
||||
|
||||
int t_beta = env_timestep_arr[kEnvId] - kBetaDelay + 1;
|
||||
if (t_beta < 0) {
|
||||
obs_a_world_lagged_stringency_level[
|
||||
kTimeIndependentArrayIdx
|
||||
] = kRealWorldStringencyPolicyHistory[
|
||||
env_timestep_arr[kEnvId] * (kNumAgents - 1) + kAgentId
|
||||
] / static_cast<float>(kNumStringencyLevels);
|
||||
} else {
|
||||
obs_a_world_lagged_stringency_level[
|
||||
kTimeIndependentArrayIdx
|
||||
] = stringency_level[
|
||||
kArrayIndexOffset +
|
||||
t_beta * (kNumAgents - 1) +
|
||||
kAgentId
|
||||
] / static_cast<float>(kNumStringencyLevels);
|
||||
}
|
||||
obs_p_world_lagged_stringency_level[
|
||||
kTimeIndependentArrayIdx
|
||||
] = obs_a_world_lagged_stringency_level[
|
||||
kTimeIndependentArrayIdx];
|
||||
// Below, we assume observation scaling = True
|
||||
// (otherwise, 'obs_a_time[kTimeIndependentArrayIdx] =
|
||||
// static_cast<float>(env_timestep_arr[kEnvId])
|
||||
obs_a_time[kTimeIndependentArrayIdx] =
|
||||
env_timestep_arr[kEnvId] / static_cast<float>(kEpisodeLength);
|
||||
} else if (kAgentId == kNumAgents - 1) {
|
||||
obs_p_time[kEnvId] = env_timestep_arr[kEnvId] /
|
||||
static_cast<float>(kEpisodeLength);
|
||||
}
|
||||
}
|
||||
|
||||
// CUDA version of the compute_reward() in
|
||||
// "ai_economist.foundation.scenarios.covid19_env.py"
|
||||
__global__ void CudaComputeReward(
|
||||
float* rewards_a,
|
||||
float* rewards_p,
|
||||
const int kNumDaysInAnYear,
|
||||
const int kValueOfLife,
|
||||
const float kRiskFreeInterestRate,
|
||||
const float kEconomicRewardCrraEta,
|
||||
const float* kMinMarginalAgentHealthIndex,
|
||||
const float* kMaxMarginalAgentHealthIndex,
|
||||
const float* kMinMarginalAgentEconomicIndex,
|
||||
const float* kMaxMarginalAgentEconomicIndex,
|
||||
const float kMinMarginalPlannerHealthIndex,
|
||||
const float kMaxMarginalPlannerHealthIndex,
|
||||
const float kMinMarginalPlannerEconomicIndex,
|
||||
const float kMaxMarginalPlannerEconomicIndex,
|
||||
const float* kWeightageOnMarginalAgentHealthIndex,
|
||||
const float* kWeightageOnMarginalPlannerHealthIndex,
|
||||
const float kWeightageOnMarginalAgentEconomicIndex,
|
||||
const float kWeightageOnMarginalPlannerEconomicIndex,
|
||||
const float* kAgentsHealthNorm,
|
||||
const float* kAgentsEconomicNorm,
|
||||
const float kPlannerHealthNorm,
|
||||
const float kPlannerEconomicNorm,
|
||||
float* deaths,
|
||||
float* subsidy,
|
||||
float* postsubsidy_productivity,
|
||||
int* env_done_arr,
|
||||
int* env_timestep_arr,
|
||||
const int kNumAgents,
|
||||
const int kEpisodeLength
|
||||
) {
|
||||
const int kEnvId = blockIdx.x;
|
||||
const int kAgentId = threadIdx.x;
|
||||
|
||||
assert(env_timestep_arr[kEnvId] > 0 &&
|
||||
env_timestep_arr[kEnvId] <= kEpisodeLength);
|
||||
assert (kAgentId <= kNumAgents - 1);
|
||||
|
||||
const int kArrayIndexOffset = kEnvId * (kEpisodeLength + 1) *
|
||||
(kNumAgents - 1);
|
||||
if (kAgentId < (kNumAgents - 1)) {
|
||||
// Agents' rewards
|
||||
// Indices for time-dependent and time-independent arrays
|
||||
// Time dependent arrays have shapes (num_envs,
|
||||
// kEpisodeLength + 1, kNumAgents - 1)
|
||||
// Time independent arrays have shapes (num_envs, kNumAgents - 1)
|
||||
int kArrayIdxCurrentTime = kArrayIndexOffset +
|
||||
env_timestep_arr[kEnvId] * (kNumAgents - 1) + kAgentId;
|
||||
int kArrayIdxPrevTime = kArrayIndexOffset +
|
||||
(env_timestep_arr[kEnvId] - 1) * (kNumAgents - 1) + kAgentId;
|
||||
const int kTimeIndependentArrayIdx = kEnvId *
|
||||
(kNumAgents - 1) + kAgentId;
|
||||
|
||||
float marginal_deaths = deaths[kArrayIdxCurrentTime] -
|
||||
deaths[kArrayIdxPrevTime];
|
||||
|
||||
// Note: changing the order of operations to prevent overflow
|
||||
float marginal_agent_health_index = - marginal_deaths /
|
||||
(kAgentsHealthNorm[kAgentId] /
|
||||
static_cast<float>(kValueOfLife));
|
||||
|
||||
float marginal_agent_economic_index = crra_nonlinearity(
|
||||
postsubsidy_productivity[kArrayIdxCurrentTime] /
|
||||
kAgentsEconomicNorm[kAgentId],
|
||||
kEconomicRewardCrraEta,
|
||||
kNumDaysInAnYear);
|
||||
|
||||
marginal_agent_health_index = min_max_normalization(
|
||||
marginal_agent_health_index,
|
||||
kMinMarginalAgentHealthIndex[kAgentId],
|
||||
kMaxMarginalAgentHealthIndex[kAgentId]);
|
||||
marginal_agent_economic_index = min_max_normalization(
|
||||
marginal_agent_economic_index,
|
||||
kMinMarginalAgentEconomicIndex[kAgentId],
|
||||
kMaxMarginalAgentEconomicIndex[kAgentId]);
|
||||
|
||||
rewards_a[kTimeIndependentArrayIdx] = get_rew(
|
||||
kWeightageOnMarginalAgentHealthIndex[kAgentId],
|
||||
marginal_agent_health_index,
|
||||
kWeightageOnMarginalPlannerHealthIndex[kAgentId],
|
||||
marginal_agent_economic_index);
|
||||
} else if (kAgentId == kNumAgents - 1) {
|
||||
// Planner's rewards
|
||||
float total_marginal_deaths = 0;
|
||||
for (int ag_id = 0; ag_id < (kNumAgents - 1); ag_id ++) {
|
||||
total_marginal_deaths += (
|
||||
deaths[kArrayIndexOffset + env_timestep_arr[kEnvId] *
|
||||
(kNumAgents - 1) + ag_id] -
|
||||
deaths[kArrayIndexOffset + (env_timestep_arr[kEnvId] - 1) *
|
||||
(kNumAgents - 1) + ag_id]);
|
||||
}
|
||||
// Note: changing the order of operations to prevent overflow
|
||||
float marginal_planner_health_index = -total_marginal_deaths /
|
||||
(kPlannerHealthNorm / static_cast<float>(kValueOfLife));
|
||||
|
||||
float total_subsidy = 0.0;
|
||||
float total_postsubsidy_productivity = 0.0;
|
||||
for (int ag_id = 0; ag_id < (kNumAgents - 1); ag_id ++) {
|
||||
total_subsidy += subsidy[kArrayIndexOffset +
|
||||
env_timestep_arr[kEnvId] * (kNumAgents - 1) + ag_id];
|
||||
total_postsubsidy_productivity +=
|
||||
postsubsidy_productivity[kArrayIndexOffset +
|
||||
env_timestep_arr[kEnvId] * (kNumAgents - 1) + ag_id];
|
||||
}
|
||||
|
||||
float cost_of_subsidy = (1 + kRiskFreeInterestRate) *
|
||||
total_subsidy;
|
||||
float marginal_planner_economic_index = crra_nonlinearity(
|
||||
(total_postsubsidy_productivity - cost_of_subsidy) /
|
||||
kPlannerEconomicNorm,
|
||||
kEconomicRewardCrraEta,
|
||||
kNumDaysInAnYear);
|
||||
|
||||
marginal_planner_health_index = min_max_normalization(
|
||||
marginal_planner_health_index,
|
||||
kMinMarginalPlannerHealthIndex,
|
||||
kMaxMarginalPlannerHealthIndex);
|
||||
marginal_planner_economic_index = min_max_normalization(
|
||||
marginal_planner_economic_index,
|
||||
kMinMarginalPlannerEconomicIndex,
|
||||
kMaxMarginalPlannerEconomicIndex);
|
||||
|
||||
rewards_p[kEnvId] = get_rew(
|
||||
kWeightageOnMarginalAgentEconomicIndex,
|
||||
marginal_planner_health_index,
|
||||
kWeightageOnMarginalPlannerEconomicIndex,
|
||||
marginal_planner_economic_index);
|
||||
}
|
||||
|
||||
// Wait here for all agents to finish computing rewards
|
||||
__syncthreads();
|
||||
|
||||
// Use only agent 0's thread to set done_arr
|
||||
if (kAgentId == 0) {
|
||||
if (env_timestep_arr[kEnvId] == kEpisodeLength) {
|
||||
env_timestep_arr[kEnvId] = 0;
|
||||
env_done_arr[kEnvId] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpgIBAAKCAQEAk1+Qz0/Qg4OOGrskBJnVI9KVGTEUvsldHUV4AzLeecYZSV5+
|
||||
FZUQpl8lq1mstUZZ0xMlGSHz2t+AAJxyEro8mAj9gAp1qeN58pAX2k29DOt4YRnp
|
||||
sTF1UG+nrV2aW+jfH16aeVsjWY+Nq+GxGyE3Q5bsxOhnOg0TUaB6RY8SBE/scTHn
|
||||
bfNsgTc5EuiAAGqYYYdu12n5zeyvfjGW7bBf4Q9t0F0bI+YdZQY9HD35KAoNcqFQ
|
||||
dvd2vKbojejkn+WyO1amnZxgAhVjpT61FV4u18jPN0Qrt0LHuF5kUVzYal+73ySY
|
||||
BbwEo4onEn9xvUlQGFJWmv4OPwbI3d4nLqP+mQIHK9xUXfK97QKCAQABeR2EO0uu
|
||||
ERyRXa5Mh7xsOEq/OJ9sQq+si8B5gDyyM1SW61wQMKF4Wiqw68bMCVvGRwScZD+T
|
||||
XwBEBJMm9lCVx/UfOWqYSNFCk/YBefv9AI0Kg5lfCMZQuTdjMcbJdjoR5xoiCbO1
|
||||
ya7oOU8mfWx/SV0o/698b/zMVBKBBQDNZaN9pmtTOgm3G1QnM9ZlmrdlKYpe9Ihs
|
||||
3sG4437QaPhumdZi8IoLBGMyYL2O38pG34LJjIkP8Efj1QVTndIIZX8CKghir++j
|
||||
nUAyofFt7/PBS2k7gQ/1gFISwHxKjmzl/Fc25o7ahlLbO+i2UnRiB9IXcmiGDXMv
|
||||
tY09oXhxCtTZAoGBAMEkMTzoiqKjXLwKLyFIF5QzXqQKcGqfC8NhQMsm43K0TgHg
|
||||
Sv1fLdnKw0FWSG30gppBorAY9p5FoI+AWwTSd+AJhz7T1y/shpJx1oBR8qKWO5kO
|
||||
gMru9kRRb0zb5hydakie3mujz7GUPiXrntKZjC4QYLar0USPulJnU+UTF6QjAoGB
|
||||
AMNWJqG1ybrk0sNkWJJDW+MnMT0T9o0E+CtbRHqMHh7K1LF9Sc/qh0gLfDo51+kr
|
||||
pscLaaJiF1Q8phzDhW9QDeNv+4lknNqMFBCFtzns1wVDlXL4U87oqhuBSs6IZAuO
|
||||
CGVefYKgefdwn64rcyRNala44BbiMJKwRoDvvgH1FvATAoGAV1YK9ZHB1RkXkZ5a
|
||||
uBePXvkScaujH4DxadMGf2tBuI1wIpVwhxOQ56yDwYoAuexXPUa8BAx2V69/LFo7
|
||||
H/yDYqzndA8WwZLy8oy7Ug+fFLtCp7VhkEwMPciBq6KjzUyShIBlgZOx5m5kTbfu
|
||||
Cs2JQU35YHeompcpLooRG1/cFZkCgYAyVlWABzmgSKJL9ohwlSBBZFCjQ1mjN6uc
|
||||
uRJxncqfCe3XQ5erFjuWMfPayWONBsWexNucJFc7Iz2LzCOXkUsftldEEET9f/2w
|
||||
PrbsEu8khNTLqUcow2Whz+A8C0dV6p2cqtTKR1XlSmNVqP30lmpHcmF+R3M/J1ON
|
||||
K7S9zJJ+zwKBgHIuCATGCGCOzAsUo80OQL46j74SxRV3H1CJASLKzatiTo54dbO6
|
||||
86w+N6BfYtYeRlnX1CTGl6bHqVUMBBlKws8Ig3gV3xFS8BiSav8zQ2m99JuhlVHF
|
||||
Ocfowmuad3WXYvYXQ5IeP2JM/3q7BoPLg1DKP4GGZlNbatMRI+H0HimV
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) 2020, salesforce.com, inc.
|
||||
# All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# For full license text, see the LICENSE file in the repo root
|
||||
# or https://opensource.org/licenses/BSD-3-Clause
|
||||
@@ -0,0 +1,336 @@
|
||||
# Copyright (c) 2021 salesforce.com, inc.
|
||||
# All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# For full license text, see the LICENSE file in the repo root
|
||||
# or https://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ai_economist.foundation.base.base_env import BaseEnvironment, scenario_registry
|
||||
from ai_economist.foundation.scenarios.utils import rewards, social_metrics
|
||||
|
||||
|
||||
@scenario_registry.add
|
||||
class OneStepEconomy(BaseEnvironment):
|
||||
"""
|
||||
A simple model featuring one "step" of setting taxes and earning income.
|
||||
|
||||
As described in https://arxiv.org/abs/2108.02755:
|
||||
A simplified version of simple_wood_and_stone scenario where both the planner
|
||||
and the agents each make a single decision: the planner setting taxes and the
|
||||
agents choosing labor. Each agent chooses an amount of labor that optimizes
|
||||
its post-tax utility, and this optimal labor depends on its skill and the tax
|
||||
rates, and it does not depend on the labor choices of other agents. Before
|
||||
the agents act, the planner sets the marginal tax rates in order to optimize
|
||||
social welfare.
|
||||
|
||||
Note:
|
||||
This scenario is intended to be used with the 'PeriodicBracketTax' and
|
||||
'SimpleLabor' components.
|
||||
It should use an episode length of 2. In the first step, taxes are set by
|
||||
the planner via 'PeriodicBracketTax'. In the second, agents select how much
|
||||
to work/earn via 'SimpleLabor'.
|
||||
|
||||
Args:
|
||||
agent_reward_type (str): The type of utility function used to compute each
|
||||
agent's reward. Defaults to "coin_minus_labor_cost".
|
||||
isoelastic_eta (float): The shape parameter of the isoelastic function used
|
||||
in the "isoelastic_coin_minus_labor" utility function.
|
||||
labor_exponent (float): The labor exponent parameter used in the
|
||||
"coin_minus_labor_cost" utility function.
|
||||
labor_cost (float): The coefficient used to weight the cost of labor.
|
||||
planner_reward_type (str): The type of social welfare function (SWF) used to
|
||||
compute the planner's reward. Defaults to "inv_income_weighted_utility".
|
||||
mixing_weight_gini_vs_coin (float): Must be between 0 and 1 (inclusive).
|
||||
Controls the weighting of equality and productivity when using SWF
|
||||
"coin_eq_times_productivity", where a value of 0 (default) yields equal
|
||||
weighting, and 1 only considers productivity.
|
||||
"""
|
||||
|
||||
name = "one-step-economy"
|
||||
agent_subclasses = ["BasicMobileAgent", "BasicPlanner"]
|
||||
required_entities = ["Coin"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*base_env_args,
|
||||
agent_reward_type="coin_minus_labor_cost",
|
||||
isoelastic_eta=0.23,
|
||||
labor_exponent=2.0,
|
||||
labor_cost=1.0,
|
||||
planner_reward_type="inv_income_weighted_utility",
|
||||
mixing_weight_gini_vs_coin=0,
|
||||
**base_env_kwargs
|
||||
):
|
||||
super().__init__(*base_env_args, **base_env_kwargs)
|
||||
|
||||
self.num_agents = len(self.world.agents)
|
||||
|
||||
self.labor_cost = labor_cost
|
||||
self.agent_reward_type = agent_reward_type
|
||||
self.isoelastic_eta = isoelastic_eta
|
||||
self.labor_exponent = labor_exponent
|
||||
self.planner_reward_type = planner_reward_type
|
||||
self.mixing_weight_gini_vs_coin = mixing_weight_gini_vs_coin
|
||||
self.planner_starting_coin = 0
|
||||
|
||||
self.curr_optimization_metrics = {str(a.idx): 0 for a in self.all_agents}
|
||||
|
||||
# The following methods must be implemented for each scenario
|
||||
# -----------------------------------------------------------
|
||||
def reset_starting_layout(self):
|
||||
"""
|
||||
Part 1/2 of scenario reset. This method handles resetting the state of the
|
||||
environment managed by the scenario (i.e. resource & landmark layout).
|
||||
|
||||
Here, generate a resource source layout consistent with target parameters.
|
||||
"""
|
||||
|
||||
def reset_agent_states(self):
|
||||
"""
|
||||
Part 2/2 of scenario reset. This method handles resetting the state of the
|
||||
agents themselves (i.e. inventory, locations, etc.).
|
||||
|
||||
Here, empty inventories, give mobile agents any starting coin, and place them
|
||||
in random accesible locations to start.
|
||||
"""
|
||||
self.world.clear_agent_locs()
|
||||
|
||||
for agent in self.world.agents:
|
||||
# Clear everything to start with
|
||||
agent.state["inventory"] = {k: 0 for k in agent.state["inventory"].keys()}
|
||||
agent.state["escrow"] = {k: 0 for k in agent.state["escrow"].keys()}
|
||||
agent.state["endogenous"] = {k: 0 for k in agent.state["endogenous"].keys()}
|
||||
|
||||
self.world.planner.inventory["Coin"] = self.planner_starting_coin
|
||||
|
||||
def scenario_step(self):
|
||||
"""
|
||||
Update the state of the world according to whatever rules this scenario
|
||||
implements.
|
||||
|
||||
This gets called in the 'step' method (of base_env) after going through each
|
||||
component step and before generating observations, rewards, etc.
|
||||
|
||||
NOTE: does not take agent actions into account.
|
||||
"""
|
||||
|
||||
def generate_observations(self):
|
||||
"""
|
||||
Generate observations associated with this scenario.
|
||||
|
||||
A scenario does not need to produce observations and can provide observations
|
||||
for only some agent types; however, for a given agent type, it should either
|
||||
always or never yield an observation. If it does yield an observation,
|
||||
that observation should always have the same structure/sizes!
|
||||
|
||||
Returns:
|
||||
obs (dict): A dictionary of {agent.idx: agent_obs_dict}. In words,
|
||||
return a dictionary with an entry for each agent (which can including
|
||||
the planner) for which this scenario provides an observation. For each
|
||||
entry, the key specifies the index of the agent and the value contains
|
||||
its associated observation dictionary.
|
||||
|
||||
Here, non-planner agents receive spatial observations (depending on the env
|
||||
config) as well as the contents of their inventory and endogenous quantities.
|
||||
The planner also receives spatial observations (again, depending on the env
|
||||
config) as well as the inventory of each of the mobile agents.
|
||||
"""
|
||||
obs_dict = dict()
|
||||
for agent in self.world.agents:
|
||||
obs_dict[str(agent.idx)] = {}
|
||||
|
||||
coin_endowments = np.array(
|
||||
[agent.total_endowment("Coin") for agent in self.world.agents]
|
||||
)
|
||||
equality = social_metrics.get_equality(coin_endowments)
|
||||
productivity = social_metrics.get_productivity(coin_endowments)
|
||||
normalized_per_capita_productivity = productivity / self.num_agents / 1000
|
||||
obs_dict[self.world.planner.idx] = {
|
||||
"normalized_per_capita_productivity": normalized_per_capita_productivity,
|
||||
"equality": equality,
|
||||
}
|
||||
|
||||
return obs_dict
|
||||
|
||||
def compute_reward(self):
|
||||
"""
|
||||
Apply the reward function(s) associated with this scenario to get the rewards
|
||||
from this step.
|
||||
|
||||
Returns:
|
||||
rew (dict): A dictionary of {agent.idx: agent_obs_dict}. In words,
|
||||
return a dictionary with an entry for each agent in the environment
|
||||
(including the planner). For each entry, the key specifies the index of
|
||||
the agent and the value contains the scalar reward earned this timestep.
|
||||
|
||||
Rewards are computed as the marginal utility (agents) or marginal social
|
||||
welfare (planner) experienced on this timestep. Ignoring discounting,
|
||||
this means that agents' (planner's) objective is to maximize the utility
|
||||
(social welfare) associated with the terminal state of the episode.
|
||||
"""
|
||||
curr_optimization_metrics = self.get_current_optimization_metrics(
|
||||
self.world.agents,
|
||||
isoelastic_eta=float(self.isoelastic_eta),
|
||||
labor_exponent=float(self.labor_exponent),
|
||||
labor_coefficient=float(self.labor_cost),
|
||||
)
|
||||
planner_agents_rew = {
|
||||
k: v - self.curr_optimization_metrics[k]
|
||||
for k, v in curr_optimization_metrics.items()
|
||||
}
|
||||
self.curr_optimization_metrics = curr_optimization_metrics
|
||||
return planner_agents_rew
|
||||
|
||||
# Optional methods for customization
|
||||
# ----------------------------------
|
||||
def additional_reset_steps(self):
|
||||
"""
|
||||
Extra scenario-specific steps that should be performed at the end of the reset
|
||||
cycle.
|
||||
|
||||
For each reset cycle...
|
||||
First, reset_starting_layout() and reset_agent_states() will be called.
|
||||
|
||||
Second, <component>.reset() will be called for each registered component.
|
||||
|
||||
Lastly, this method will be called to allow for any final customization of
|
||||
the reset cycle.
|
||||
"""
|
||||
self.curr_optimization_metrics = self.get_current_optimization_metrics(
|
||||
self.world.agents,
|
||||
isoelastic_eta=float(self.isoelastic_eta),
|
||||
labor_exponent=float(self.labor_exponent),
|
||||
labor_coefficient=float(self.labor_cost),
|
||||
)
|
||||
|
||||
def scenario_metrics(self):
|
||||
"""
|
||||
Allows the scenario to generate metrics (collected along with component metrics
|
||||
in the 'metrics' property).
|
||||
|
||||
To have the scenario add metrics, this function needs to return a dictionary of
|
||||
{metric_key: value} where 'value' is a scalar (no nesting or lists!)
|
||||
|
||||
Here, summarize social metrics, endowments, utilities, and labor cost annealing.
|
||||
"""
|
||||
metrics = dict()
|
||||
|
||||
# Log social/economic indicators
|
||||
coin_endowments = np.array(
|
||||
[agent.total_endowment("Coin") for agent in self.world.agents]
|
||||
)
|
||||
pretax_incomes = np.array(
|
||||
[agent.state["production"] for agent in self.world.agents]
|
||||
)
|
||||
metrics["social/productivity"] = social_metrics.get_productivity(
|
||||
coin_endowments
|
||||
)
|
||||
metrics["social/equality"] = social_metrics.get_equality(coin_endowments)
|
||||
|
||||
utilities = np.array(
|
||||
[self.curr_optimization_metrics[agent.idx] for agent in self.world.agents]
|
||||
)
|
||||
metrics[
|
||||
"social_welfare/coin_eq_times_productivity"
|
||||
] = rewards.coin_eq_times_productivity(
|
||||
coin_endowments=coin_endowments, equality_weight=1.0
|
||||
)
|
||||
metrics[
|
||||
"social_welfare/inv_income_weighted_utility"
|
||||
] = rewards.inv_income_weighted_utility(
|
||||
coin_endowments=pretax_incomes, utilities=utilities # coin_endowments,
|
||||
)
|
||||
|
||||
# Log average endowments, endogenous, and utility for agents
|
||||
agent_endows = {}
|
||||
agent_endogenous = {}
|
||||
agent_utilities = []
|
||||
for agent in self.world.agents:
|
||||
for resource in agent.inventory.keys():
|
||||
if resource not in agent_endows:
|
||||
agent_endows[resource] = []
|
||||
agent_endows[resource].append(
|
||||
agent.inventory[resource] + agent.escrow[resource]
|
||||
)
|
||||
|
||||
for endogenous, quantity in agent.endogenous.items():
|
||||
if endogenous not in agent_endogenous:
|
||||
agent_endogenous[endogenous] = []
|
||||
agent_endogenous[endogenous].append(quantity)
|
||||
|
||||
agent_utilities.append(self.curr_optimization_metrics[agent.idx])
|
||||
|
||||
for resource, quantities in agent_endows.items():
|
||||
metrics["endow/avg_agent/{}".format(resource)] = np.mean(quantities)
|
||||
|
||||
for endogenous, quantities in agent_endogenous.items():
|
||||
metrics["endogenous/avg_agent/{}".format(endogenous)] = np.mean(quantities)
|
||||
|
||||
metrics["util/avg_agent"] = np.mean(agent_utilities)
|
||||
|
||||
# Log endowments and utility for the planner
|
||||
for resource, quantity in self.world.planner.inventory.items():
|
||||
metrics["endow/p/{}".format(resource)] = quantity
|
||||
|
||||
metrics["util/p"] = self.curr_optimization_metrics[self.world.planner.idx]
|
||||
|
||||
return metrics
|
||||
|
||||
def get_current_optimization_metrics(
|
||||
self, agents, isoelastic_eta=0.23, labor_exponent=2.0, labor_coefficient=0.1
|
||||
):
|
||||
"""
|
||||
Compute optimization metrics based on the current state. Used to compute reward.
|
||||
|
||||
Returns:
|
||||
curr_optimization_metric (dict): A dictionary of {agent.idx: metric}
|
||||
with an entry for each agent (including the planner) in the env.
|
||||
"""
|
||||
curr_optimization_metric = {}
|
||||
|
||||
coin_endowments = np.array([agent.total_endowment("Coin") for agent in agents])
|
||||
|
||||
pretax_incomes = np.array([agent.state["production"] for agent in agents])
|
||||
|
||||
# Optimization metric for agents:
|
||||
for agent in agents:
|
||||
if self.agent_reward_type == "isoelastic_coin_minus_labor":
|
||||
assert 0.0 <= isoelastic_eta <= 1.0
|
||||
curr_optimization_metric[
|
||||
agent.idx
|
||||
] = rewards.isoelastic_coin_minus_labor(
|
||||
coin_endowment=agent.total_endowment("Coin"),
|
||||
total_labor=agent.state["endogenous"]["Labor"],
|
||||
isoelastic_eta=isoelastic_eta,
|
||||
labor_coefficient=labor_coefficient,
|
||||
)
|
||||
elif self.agent_reward_type == "coin_minus_labor_cost":
|
||||
assert labor_exponent > 1.0
|
||||
curr_optimization_metric[agent.idx] = rewards.coin_minus_labor_cost(
|
||||
coin_endowment=agent.total_endowment("Coin"),
|
||||
total_labor=agent.state["endogenous"]["Labor"],
|
||||
labor_exponent=labor_exponent,
|
||||
labor_coefficient=labor_coefficient,
|
||||
)
|
||||
# Optimization metric for the planner:
|
||||
if self.planner_reward_type == "coin_eq_times_productivity":
|
||||
curr_optimization_metric[
|
||||
self.world.planner.idx
|
||||
] = rewards.coin_eq_times_productivity(
|
||||
coin_endowments=coin_endowments,
|
||||
equality_weight=1 - self.mixing_weight_gini_vs_coin,
|
||||
)
|
||||
elif self.planner_reward_type == "inv_income_weighted_utility":
|
||||
curr_optimization_metric[
|
||||
self.world.planner.idx
|
||||
] = rewards.inv_income_weighted_utility(
|
||||
coin_endowments=pretax_incomes, # coin_endowments,
|
||||
utilities=np.array(
|
||||
[curr_optimization_metric[agent.idx] for agent in agents]
|
||||
),
|
||||
)
|
||||
else:
|
||||
print("No valid planner reward selected!")
|
||||
raise NotImplementedError
|
||||
return curr_optimization_metric
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) 2020, salesforce.com, inc.
|
||||
# All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# For full license text, see the LICENSE file in the repo root
|
||||
# or https://opensource.org/licenses/BSD-3-Clause
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,800 @@
|
||||
# Copyright (c) 2020, salesforce.com, inc.
|
||||
# All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# For full license text, see the LICENSE file in the repo root
|
||||
# or https://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from scipy import signal
|
||||
|
||||
from ai_economist.foundation.base.base_env import BaseEnvironment, scenario_registry
|
||||
from ai_economist.foundation.scenarios.utils import rewards, social_metrics
|
||||
|
||||
|
||||
@scenario_registry.add
|
||||
class LayoutFromFile(BaseEnvironment):
|
||||
"""
|
||||
World containing stone and wood with stochastic regeneration. Refers to a fixed
|
||||
layout file (see ./map_txt/ for examples) to determine the spatial arrangement of
|
||||
stone, wood, and water tiles.
|
||||
|
||||
Args:
|
||||
planner_gets_spatial_obs (bool): Whether the planner agent receives spatial
|
||||
observations from the world.
|
||||
full_observability (bool): Whether the mobile agents' spatial observation
|
||||
includes the full world view or is instead an egocentric view.
|
||||
mobile_agent_observation_range (int): If not using full_observability,
|
||||
the spatial range (on each side of the agent) that is visible in the
|
||||
spatial observations.
|
||||
env_layout_file (str): Name of the layout file in ./map_txt/ to use.
|
||||
Note: The world dimensions of that layout must match the world dimensions
|
||||
argument used to construct the environment.
|
||||
resource_regen_prob (float): Probability that an empty source tile will
|
||||
regenerate a new resource unit.
|
||||
fixed_four_skill_and_loc (bool): Whether to use a fixed set of build skills and
|
||||
starting locations, with agents grouped into starting locations based on
|
||||
which skill quartile they are in. False, by default.
|
||||
True, for experiments in https://arxiv.org/abs/2004.13332.
|
||||
Note: Requires that the environment uses the "Build" component with
|
||||
skill_dist="pareto".
|
||||
starting_agent_coin (int, float): Amount of coin agents have at t=0. Defaults
|
||||
to zero coin.
|
||||
isoelastic_eta (float): Parameter controlling the shape of agent utility
|
||||
wrt coin endowment.
|
||||
energy_cost (float): Coefficient for converting labor to negative utility.
|
||||
energy_warmup_constant (float): Decay constant that controls the rate at which
|
||||
the effective energy cost is annealed from 0 to energy_cost. Set to 0
|
||||
(default) to disable annealing, meaning that the effective energy cost is
|
||||
always energy_cost. The units of the decay constant depend on the choice of
|
||||
energy_warmup_method.
|
||||
energy_warmup_method (str): How to schedule energy annealing (warmup). If
|
||||
"decay" (default), use the number of completed episodes. If "auto",
|
||||
use the number of timesteps where the average agent reward was positive.
|
||||
planner_reward_type (str): The type of reward used for the planner. Options
|
||||
are "coin_eq_times_productivity" (default),
|
||||
"inv_income_weighted_coin_endowment", and "inv_income_weighted_utility".
|
||||
mixing_weight_gini_vs_coin (float): Degree to which equality is ignored w/
|
||||
"coin_eq_times_productivity". Default is 0, which weights equality and
|
||||
productivity equally. If set to 1, only productivity is rewarded.
|
||||
"""
|
||||
|
||||
name = "layout_from_file/simple_wood_and_stone"
|
||||
agent_subclasses = ["BasicMobileAgent", "BasicPlanner"]
|
||||
required_entities = ["Wood", "Stone", "Water"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*base_env_args,
|
||||
planner_gets_spatial_info=True,
|
||||
full_observability=False,
|
||||
mobile_agent_observation_range=5,
|
||||
env_layout_file="quadrant_25x25_20each_30clump.txt",
|
||||
resource_regen_prob=0.01,
|
||||
fixed_four_skill_and_loc=False,
|
||||
starting_agent_coin=0,
|
||||
isoelastic_eta=0.23,
|
||||
energy_cost=0.21,
|
||||
energy_warmup_constant=0,
|
||||
energy_warmup_method="decay",
|
||||
planner_reward_type="coin_eq_times_productivity",
|
||||
mixing_weight_gini_vs_coin=0.0,
|
||||
**base_env_kwargs,
|
||||
):
|
||||
super().__init__(*base_env_args, **base_env_kwargs)
|
||||
|
||||
# Whether agents receive spatial information in their observation tensor
|
||||
self._planner_gets_spatial_info = bool(planner_gets_spatial_info)
|
||||
|
||||
# Whether the (non-planner) agents can see the whole world map
|
||||
self._full_observability = bool(full_observability)
|
||||
|
||||
self._mobile_agent_observation_range = int(mobile_agent_observation_range)
|
||||
|
||||
# Load in the layout
|
||||
path_to_layout_file = Path(f"{Path(__file__).parent}/map_txt/{env_layout_file}")
|
||||
|
||||
with open(path_to_layout_file, "r") as f:
|
||||
self.env_layout_string = f.read()
|
||||
self.env_layout = self.env_layout_string.split(";")
|
||||
|
||||
# Convert the layout to landmark maps
|
||||
landmark_lookup = {"W": "Wood", "S": "Stone", "@": "Water"}
|
||||
self._source_maps = {
|
||||
r: np.zeros(self.world_size) for r in landmark_lookup.values()
|
||||
}
|
||||
for r, symbol_row in enumerate(self.env_layout):
|
||||
for c, symbol in enumerate(symbol_row):
|
||||
landmark = landmark_lookup.get(symbol, None)
|
||||
if landmark:
|
||||
self._source_maps[landmark][r, c] = 1
|
||||
|
||||
# For controlling how resource regeneration behavior
|
||||
self.layout_specs = dict(
|
||||
Wood={
|
||||
"regen_weight": float(resource_regen_prob),
|
||||
"regen_halfwidth": 0,
|
||||
"max_health": 1,
|
||||
},
|
||||
Stone={
|
||||
"regen_weight": float(resource_regen_prob),
|
||||
"regen_halfwidth": 0,
|
||||
"max_health": 1,
|
||||
},
|
||||
)
|
||||
assert 0 <= self.layout_specs["Wood"]["regen_weight"] <= 1
|
||||
assert 0 <= self.layout_specs["Stone"]["regen_weight"] <= 1
|
||||
|
||||
# How much coin do agents begin with at upon reset
|
||||
self.starting_agent_coin = float(starting_agent_coin)
|
||||
assert self.starting_agent_coin >= 0.0
|
||||
|
||||
# Controls the diminishing marginal utility of coin.
|
||||
# isoelastic_eta=0 means no diminishing utility.
|
||||
self.isoelastic_eta = float(isoelastic_eta)
|
||||
assert 0.0 <= self.isoelastic_eta <= 1.0
|
||||
|
||||
# The amount that labor is weighted in utility computation
|
||||
# (once annealing is finished)
|
||||
self.energy_cost = float(energy_cost)
|
||||
assert self.energy_cost >= 0
|
||||
|
||||
# Which method to use for calculating the progress of energy annealing
|
||||
# If method = 'decay': #completed episodes
|
||||
# If method = 'auto' : #timesteps where avg. agent reward > 0
|
||||
self.energy_warmup_method = energy_warmup_method.lower()
|
||||
assert self.energy_warmup_method in ["decay", "auto"]
|
||||
# Decay constant for annealing to full energy cost
|
||||
# (if energy_warmup_constant == 0, there is no annealing)
|
||||
self.energy_warmup_constant = float(energy_warmup_constant)
|
||||
assert self.energy_warmup_constant >= 0
|
||||
self._auto_warmup_integrator = 0
|
||||
|
||||
# Which social welfare function to use
|
||||
self.planner_reward_type = str(planner_reward_type).lower()
|
||||
|
||||
# How much to weight equality if using SWF=eq*prod:
|
||||
# 0 -> SWF=eq * prod
|
||||
# 1 -> SWF=prod
|
||||
self.mixing_weight_gini_vs_coin = float(mixing_weight_gini_vs_coin)
|
||||
assert 0 <= self.mixing_weight_gini_vs_coin <= 1.0
|
||||
|
||||
# Use this to calculate marginal changes and deliver that as reward
|
||||
self.init_optimization_metric = {agent.idx: 0 for agent in self.all_agents}
|
||||
self.prev_optimization_metric = {agent.idx: 0 for agent in self.all_agents}
|
||||
self.curr_optimization_metric = {agent.idx: 0 for agent in self.all_agents}
|
||||
|
||||
"""
|
||||
Fixed Four Skill and Loc
|
||||
------------------------
|
||||
"""
|
||||
self.agent_starting_pos = {agent.idx: [] for agent in self.world.agents}
|
||||
|
||||
self.fixed_four_skill_and_loc = bool(fixed_four_skill_and_loc)
|
||||
if self.fixed_four_skill_and_loc:
|
||||
bm = self.get_component("Build")
|
||||
assert bm.skill_dist == "pareto"
|
||||
pmsm = bm.payment_max_skill_multiplier
|
||||
|
||||
# Temporarily switch to a fixed seed for controlling randomness
|
||||
seed_state = np.random.get_state()
|
||||
np.random.seed(seed=1)
|
||||
|
||||
# Generate a batch (100000) of num_agents (sorted/clipped) Pareto samples.
|
||||
pareto_samples = np.random.pareto(4, size=(100000, self.n_agents))
|
||||
clipped_skills = np.minimum(pmsm, (pmsm - 1) * pareto_samples + 1)
|
||||
sorted_clipped_skills = np.sort(clipped_skills, axis=1)
|
||||
# The skill level of the i-th skill-ranked agent is the average of the
|
||||
# i-th ranked samples throughout the batch.
|
||||
average_ranked_skills = sorted_clipped_skills.mean(axis=0)
|
||||
self._avg_ranked_skill = average_ranked_skills * bm.payment
|
||||
|
||||
np.random.set_state(seed_state)
|
||||
|
||||
# Fill in the starting location associated with each skill rank
|
||||
starting_ranked_locs = [
|
||||
# Worst group of agents goes in top right
|
||||
(0, self.world_size[1] - 1),
|
||||
# Second-worst group of agents goes in bottom left
|
||||
(self.world_size[0] - 1, 0),
|
||||
# Second-best group of agents goes in top left
|
||||
(0, 0),
|
||||
# Best group of agents goes in bottom right
|
||||
(self.world_size[1] - 1, self.world_size[1] - 1),
|
||||
]
|
||||
self._ranked_locs = []
|
||||
|
||||
# Based on skill, assign each agent to one of the location groups
|
||||
skill_groups = np.floor(
|
||||
np.arange(self.n_agents) * (4 / self.n_agents),
|
||||
).astype(np.int)
|
||||
n_in_group = np.zeros(4, dtype=np.int)
|
||||
for g in skill_groups:
|
||||
# The position within the group is given by the number of agents
|
||||
# counted in the group thus far.
|
||||
g_pos = n_in_group[g]
|
||||
|
||||
# Top right
|
||||
if g == 0:
|
||||
r = starting_ranked_locs[g][0] + (g_pos // 4)
|
||||
c = starting_ranked_locs[g][1] - (g_pos % 4)
|
||||
self._ranked_locs.append((r, c))
|
||||
|
||||
# Bottom left
|
||||
elif g == 1:
|
||||
r = starting_ranked_locs[g][0] - (g_pos // 4)
|
||||
c = starting_ranked_locs[g][1] + (g_pos % 4)
|
||||
self._ranked_locs.append((r, c))
|
||||
|
||||
# Top left
|
||||
elif g == 2:
|
||||
r = starting_ranked_locs[g][0] + (g_pos // 4)
|
||||
c = starting_ranked_locs[g][1] + (g_pos % 4)
|
||||
self._ranked_locs.append((r, c))
|
||||
|
||||
# Bottom right
|
||||
elif g == 3:
|
||||
r = starting_ranked_locs[g][0] - (g_pos // 4)
|
||||
c = starting_ranked_locs[g][1] - (g_pos % 4)
|
||||
self._ranked_locs.append((r, c))
|
||||
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
# Count the agent we just placed.
|
||||
n_in_group[g] = n_in_group[g] + 1
|
||||
|
||||
@property
|
||||
def energy_weight(self):
|
||||
"""
|
||||
Energy annealing progress. Multiply with self.energy_cost to get the
|
||||
effective energy coefficient.
|
||||
"""
|
||||
if self.energy_warmup_constant <= 0.0:
|
||||
return 1.0
|
||||
|
||||
if self.energy_warmup_method == "decay":
|
||||
return float(1.0 - np.exp(-self._completions / self.energy_warmup_constant))
|
||||
|
||||
if self.energy_warmup_method == "auto":
|
||||
return float(
|
||||
1.0
|
||||
- np.exp(-self._auto_warmup_integrator / self.energy_warmup_constant)
|
||||
)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def get_current_optimization_metrics(self):
|
||||
"""
|
||||
Compute optimization metrics based on the current state. Used to compute reward.
|
||||
|
||||
Returns:
|
||||
curr_optimization_metric (dict): A dictionary of {agent.idx: metric}
|
||||
with an entry for each agent (including the planner) in the env.
|
||||
"""
|
||||
curr_optimization_metric = {}
|
||||
# (for agents)
|
||||
for agent in self.world.agents:
|
||||
curr_optimization_metric[agent.idx] = rewards.isoelastic_coin_minus_labor(
|
||||
coin_endowment=agent.total_endowment("Coin"),
|
||||
total_labor=agent.state["endogenous"]["Labor"],
|
||||
isoelastic_eta=self.isoelastic_eta,
|
||||
labor_coefficient=self.energy_weight * self.energy_cost,
|
||||
)
|
||||
# (for the planner)
|
||||
if self.planner_reward_type == "coin_eq_times_productivity":
|
||||
curr_optimization_metric[
|
||||
self.world.planner.idx
|
||||
] = rewards.coin_eq_times_productivity(
|
||||
coin_endowments=np.array(
|
||||
[agent.total_endowment("Coin") for agent in self.world.agents]
|
||||
),
|
||||
equality_weight=1 - self.mixing_weight_gini_vs_coin,
|
||||
)
|
||||
elif self.planner_reward_type == "inv_income_weighted_coin_endowments":
|
||||
curr_optimization_metric[
|
||||
self.world.planner.idx
|
||||
] = rewards.inv_income_weighted_coin_endowments(
|
||||
coin_endowments=np.array(
|
||||
[agent.total_endowment("Coin") for agent in self.world.agents]
|
||||
)
|
||||
)
|
||||
elif self.planner_reward_type == "inv_income_weighted_utility":
|
||||
curr_optimization_metric[
|
||||
self.world.planner.idx
|
||||
] = rewards.inv_income_weighted_utility(
|
||||
coin_endowments=np.array(
|
||||
[agent.total_endowment("Coin") for agent in self.world.agents]
|
||||
),
|
||||
utilities=np.array(
|
||||
[curr_optimization_metric[agent.idx] for agent in self.world.agents]
|
||||
),
|
||||
)
|
||||
else:
|
||||
print("No valid planner reward selected!")
|
||||
raise NotImplementedError
|
||||
return curr_optimization_metric
|
||||
|
||||
# The following methods must be implemented for each scenario
|
||||
# -----------------------------------------------------------
|
||||
|
||||
def reset_starting_layout(self):
|
||||
"""
|
||||
Part 1/2 of scenario reset. This method handles resetting the state of the
|
||||
environment managed by the scenario (i.e. resource & landmark layout).
|
||||
|
||||
Here, reset to the layout in the fixed layout file
|
||||
"""
|
||||
self.world.maps.clear()
|
||||
for landmark, landmark_map in self._source_maps.items():
|
||||
self.world.maps.set(landmark, landmark_map)
|
||||
if landmark in ["Stone", "Wood"]:
|
||||
self.world.maps.set(landmark + "SourceBlock", landmark_map)
|
||||
|
||||
def reset_agent_states(self):
|
||||
"""
|
||||
Part 2/2 of scenario reset. This method handles resetting the state of the
|
||||
agents themselves (i.e. inventory, locations, etc.).
|
||||
|
||||
Here, empty inventories and place mobile agents in random, accessible
|
||||
locations to start. Note: If using fixed_four_skill_and_loc, the starting
|
||||
locations will be overridden in self.additional_reset_steps.
|
||||
"""
|
||||
self.world.clear_agent_locs()
|
||||
for agent in self.world.agents:
|
||||
agent.state["inventory"] = {k: 0 for k in agent.inventory.keys()}
|
||||
agent.state["escrow"] = {k: 0 for k in agent.inventory.keys()}
|
||||
agent.state["endogenous"] = {k: 0 for k in agent.endogenous.keys()}
|
||||
# Add starting coin
|
||||
agent.state["inventory"]["Coin"] = float(self.starting_agent_coin)
|
||||
|
||||
self.world.planner.state["inventory"] = {
|
||||
k: 0 for k in self.world.planner.inventory.keys()
|
||||
}
|
||||
self.world.planner.state["escrow"] = {
|
||||
k: 0 for k in self.world.planner.escrow.keys()
|
||||
}
|
||||
|
||||
for agent in self.world.agents:
|
||||
r = np.random.randint(0, self.world_size[0])
|
||||
c = np.random.randint(0, self.world_size[1])
|
||||
n_tries = 0
|
||||
while not self.world.can_agent_occupy(r, c, agent):
|
||||
r = np.random.randint(0, self.world_size[0])
|
||||
c = np.random.randint(0, self.world_size[1])
|
||||
n_tries += 1
|
||||
if n_tries > 200:
|
||||
raise TimeoutError
|
||||
r, c = self.world.set_agent_loc(agent, r, c)
|
||||
|
||||
def scenario_step(self):
|
||||
"""
|
||||
Update the state of the world according to whatever rules this scenario
|
||||
implements.
|
||||
|
||||
This gets called in the 'step' method (of base_env) after going through each
|
||||
component step and before generating observations, rewards, etc.
|
||||
|
||||
In this class of scenarios, the scenario step handles stochastic resource
|
||||
regeneration.
|
||||
"""
|
||||
|
||||
resources = ["Wood", "Stone"]
|
||||
|
||||
for resource in resources:
|
||||
d = 1 + (2 * self.layout_specs[resource]["regen_halfwidth"])
|
||||
kernel = (
|
||||
self.layout_specs[resource]["regen_weight"] * np.ones((d, d)) / (d ** 2)
|
||||
)
|
||||
|
||||
resource_map = self.world.maps.get(resource)
|
||||
resource_source_blocks = self.world.maps.get(resource + "SourceBlock")
|
||||
spawnable = (
|
||||
self.world.maps.empty + resource_map + resource_source_blocks
|
||||
) > 0
|
||||
spawnable *= resource_source_blocks > 0
|
||||
|
||||
health = np.maximum(resource_map, resource_source_blocks)
|
||||
respawn = np.random.rand(*health.shape) < signal.convolve2d(
|
||||
health, kernel, "same"
|
||||
)
|
||||
respawn *= spawnable
|
||||
|
||||
self.world.maps.set(
|
||||
resource,
|
||||
np.minimum(
|
||||
resource_map + respawn, self.layout_specs[resource]["max_health"]
|
||||
),
|
||||
)
|
||||
|
||||
def generate_observations(self):
|
||||
"""
|
||||
Generate observations associated with this scenario.
|
||||
|
||||
A scenario does not need to produce observations and can provide observations
|
||||
for only some agent types; however, for a given agent type, it should either
|
||||
always or never yield an observation. If it does yield an observation,
|
||||
that observation should always have the same structure/sizes!
|
||||
|
||||
Returns:
|
||||
obs (dict): A dictionary of {agent.idx: agent_obs_dict}. In words,
|
||||
return a dictionary with an entry for each agent (which can including
|
||||
the planner) for which this scenario provides an observation. For each
|
||||
entry, the key specifies the index of the agent and the value contains
|
||||
its associated observation dictionary.
|
||||
|
||||
Here, non-planner agents receive spatial observations (depending on the env
|
||||
config) as well as the contents of their inventory and endogenous quantities.
|
||||
The planner also receives spatial observations (again, depending on the env
|
||||
config) as well as the inventory of each of the mobile agents.
|
||||
"""
|
||||
obs = {}
|
||||
curr_map = self.world.maps.state
|
||||
|
||||
owner_map = self.world.maps.owner_state
|
||||
loc_map = self.world.loc_map
|
||||
agent_idx_maps = np.concatenate([owner_map, loc_map[None, :, :]], axis=0)
|
||||
agent_idx_maps += 2
|
||||
agent_idx_maps[agent_idx_maps == 1] = 0
|
||||
|
||||
agent_locs = {
|
||||
str(agent.idx): {
|
||||
"loc-row": agent.loc[0] / self.world_size[0],
|
||||
"loc-col": agent.loc[1] / self.world_size[1],
|
||||
}
|
||||
for agent in self.world.agents
|
||||
}
|
||||
agent_invs = {
|
||||
str(agent.idx): {
|
||||
"inventory-" + k: v * self.inv_scale for k, v in agent.inventory.items()
|
||||
}
|
||||
for agent in self.world.agents
|
||||
}
|
||||
|
||||
obs[self.world.planner.idx] = {
|
||||
"inventory-" + k: v * self.inv_scale
|
||||
for k, v in self.world.planner.inventory.items()
|
||||
}
|
||||
if self._planner_gets_spatial_info:
|
||||
obs[self.world.planner.idx].update(
|
||||
dict(map=curr_map, idx_map=agent_idx_maps)
|
||||
)
|
||||
|
||||
# Mobile agents see the full map. Convey location info via one-hot map channels.
|
||||
if self._full_observability:
|
||||
for agent in self.world.agents:
|
||||
my_map = np.array(agent_idx_maps)
|
||||
my_map[my_map == int(agent.idx) + 2] = 1
|
||||
sidx = str(agent.idx)
|
||||
obs[sidx] = {"map": curr_map, "idx_map": my_map}
|
||||
obs[sidx].update(agent_invs[sidx])
|
||||
|
||||
# Mobile agents only see within a window around their position
|
||||
else:
|
||||
w = (
|
||||
self._mobile_agent_observation_range
|
||||
) # View halfwidth (only applicable without full observability)
|
||||
|
||||
padded_map = np.pad(
|
||||
curr_map,
|
||||
[(0, 1), (w, w), (w, w)],
|
||||
mode="constant",
|
||||
constant_values=[(0, 1), (0, 0), (0, 0)],
|
||||
)
|
||||
|
||||
padded_idx = np.pad(
|
||||
agent_idx_maps,
|
||||
[(0, 0), (w, w), (w, w)],
|
||||
mode="constant",
|
||||
constant_values=[(0, 0), (0, 0), (0, 0)],
|
||||
)
|
||||
|
||||
for agent in self.world.agents:
|
||||
r, c = [c + w for c in agent.loc]
|
||||
visible_map = padded_map[
|
||||
:, (r - w) : (r + w + 1), (c - w) : (c + w + 1)
|
||||
]
|
||||
visible_idx = np.array(
|
||||
padded_idx[:, (r - w) : (r + w + 1), (c - w) : (c + w + 1)]
|
||||
)
|
||||
|
||||
visible_idx[visible_idx == int(agent.idx) + 2] = 1
|
||||
|
||||
sidx = str(agent.idx)
|
||||
|
||||
obs[sidx] = {"map": visible_map, "idx_map": visible_idx}
|
||||
obs[sidx].update(agent_locs[sidx])
|
||||
obs[sidx].update(agent_invs[sidx])
|
||||
|
||||
# Agent-wise planner info (gets crunched into the planner obs in the
|
||||
# base scenario code)
|
||||
obs["p" + sidx] = agent_invs[sidx]
|
||||
if self._planner_gets_spatial_info:
|
||||
obs["p" + sidx].update(agent_locs[sidx])
|
||||
|
||||
return obs
|
||||
|
||||
def compute_reward(self):
|
||||
"""
|
||||
Apply the reward function(s) associated with this scenario to get the rewards
|
||||
from this step.
|
||||
|
||||
Returns:
|
||||
rew (dict): A dictionary of {agent.idx: agent_obs_dict}. In words,
|
||||
return a dictionary with an entry for each agent in the environment
|
||||
(including the planner). For each entry, the key specifies the index of
|
||||
the agent and the value contains the scalar reward earned this timestep.
|
||||
|
||||
Rewards are computed as the marginal utility (agents) or marginal social
|
||||
welfare (planner) experienced on this timestep. Ignoring discounting,
|
||||
this means that agents' (planner's) objective is to maximize the utility
|
||||
(social welfare) associated with the terminal state of the episode.
|
||||
"""
|
||||
|
||||
# "curr_optimization_metric" hasn't been updated yet, so it gives us the
|
||||
# utility from the last step.
|
||||
utility_at_end_of_last_time_step = deepcopy(self.curr_optimization_metric)
|
||||
|
||||
# compute current objectives and store the values
|
||||
self.curr_optimization_metric = self.get_current_optimization_metrics()
|
||||
|
||||
# reward = curr - prev objectives
|
||||
rew = {
|
||||
k: float(v - utility_at_end_of_last_time_step[k])
|
||||
for k, v in self.curr_optimization_metric.items()
|
||||
}
|
||||
|
||||
# store the previous objective values
|
||||
self.prev_optimization_metric.update(utility_at_end_of_last_time_step)
|
||||
|
||||
# Automatic Energy Cost Annealing
|
||||
# -------------------------------
|
||||
avg_agent_rew = np.mean([rew[a.idx] for a in self.world.agents])
|
||||
# Count the number of timesteps where the avg agent reward was > 0
|
||||
if avg_agent_rew > 0:
|
||||
self._auto_warmup_integrator += 1
|
||||
|
||||
return rew
|
||||
|
||||
# Optional methods for customization
|
||||
# ----------------------------------
|
||||
|
||||
def additional_reset_steps(self):
|
||||
"""
|
||||
Extra scenario-specific steps that should be performed at the end of the reset
|
||||
cycle.
|
||||
|
||||
For each reset cycle...
|
||||
First, reset_starting_layout() and reset_agent_states() will be called.
|
||||
|
||||
Second, <component>.reset() will be called for each registered component.
|
||||
|
||||
Lastly, this method will be called to allow for any final customization of
|
||||
the reset cycle.
|
||||
|
||||
For this scenario, this method resets optimization metric trackers. If using
|
||||
fixed_four_skill_and_loc, this is where each agent gets assigned to one of
|
||||
the four fixed skill/loc combinations. The agent-->skill/loc assignment is
|
||||
permuted so that all four skill/loc combinations are used.
|
||||
"""
|
||||
if self.fixed_four_skill_and_loc:
|
||||
self.world.clear_agent_locs()
|
||||
for i, agent in enumerate(self.world.get_random_order_agents()):
|
||||
self.world.set_agent_loc(agent, *self._ranked_locs[i])
|
||||
agent.state["build_payment"] = self._avg_ranked_skill[i]
|
||||
|
||||
# compute current objectives
|
||||
curr_optimization_metric = self.get_current_optimization_metrics()
|
||||
|
||||
self.curr_optimization_metric = deepcopy(curr_optimization_metric)
|
||||
self.init_optimization_metric = deepcopy(curr_optimization_metric)
|
||||
self.prev_optimization_metric = deepcopy(curr_optimization_metric)
|
||||
|
||||
def scenario_metrics(self):
|
||||
"""
|
||||
Allows the scenario to generate metrics (collected along with component metrics
|
||||
in the 'metrics' property).
|
||||
|
||||
To have the scenario add metrics, this function needs to return a dictionary of
|
||||
{metric_key: value} where 'value' is a scalar (no nesting or lists!)
|
||||
|
||||
Here, summarize social metrics, endowments, utilities, and labor cost annealing.
|
||||
"""
|
||||
metrics = dict()
|
||||
|
||||
coin_endowments = np.array(
|
||||
[agent.total_endowment("Coin") for agent in self.world.agents]
|
||||
)
|
||||
metrics["social/productivity"] = social_metrics.get_productivity(
|
||||
coin_endowments
|
||||
)
|
||||
metrics["social/equality"] = social_metrics.get_equality(coin_endowments)
|
||||
|
||||
utilities = np.array(
|
||||
[self.curr_optimization_metric[agent.idx] for agent in self.world.agents]
|
||||
)
|
||||
metrics[
|
||||
"social_welfare/coin_eq_times_productivity"
|
||||
] = rewards.coin_eq_times_productivity(
|
||||
coin_endowments=coin_endowments, equality_weight=1.0
|
||||
)
|
||||
metrics[
|
||||
"social_welfare/inv_income_weighted_coin_endow"
|
||||
] = rewards.inv_income_weighted_coin_endowments(coin_endowments=coin_endowments)
|
||||
metrics[
|
||||
"social_welfare/inv_income_weighted_utility"
|
||||
] = rewards.inv_income_weighted_utility(
|
||||
coin_endowments=coin_endowments, utilities=utilities
|
||||
)
|
||||
|
||||
for agent in self.all_agents:
|
||||
for resource, quantity in agent.inventory.items():
|
||||
metrics[
|
||||
"endow/{}/{}".format(agent.idx, resource)
|
||||
] = agent.total_endowment(resource)
|
||||
|
||||
if agent.endogenous is not None:
|
||||
for resource, quantity in agent.endogenous.items():
|
||||
metrics["endogenous/{}/{}".format(agent.idx, resource)] = quantity
|
||||
|
||||
metrics["util/{}".format(agent.idx)] = self.curr_optimization_metric[
|
||||
agent.idx
|
||||
]
|
||||
|
||||
# Labor weight
|
||||
metrics["labor/weighted_cost"] = self.energy_cost * self.energy_weight
|
||||
metrics["labor/warmup_integrator"] = int(self._auto_warmup_integrator)
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
@scenario_registry.add
|
||||
class SplitLayout(LayoutFromFile):
|
||||
"""
|
||||
Extends layout_from_file/simple_wood_and_stone to impose a row of water midway
|
||||
through the map, uses a fixed set of pareto-distributed building skills (requires a
|
||||
Build component), and places agents in the top/bottom depending on skill rank.
|
||||
|
||||
Args:
|
||||
water_row (int): Row of the map where the water barrier is placed. Defaults
|
||||
to half the world height.
|
||||
skill_rank_of_top_agents (int, float, tuple, list): Index/indices specifying
|
||||
which agent(s) to place in the top of the map. Indices refer to the skill
|
||||
ranking, with 0 referring to the highest-skilled agent. Defaults to only
|
||||
the highest-skilled agent in the top.
|
||||
planner_gets_spatial_obs (bool): Whether the planner agent receives spatial
|
||||
observations from the world.
|
||||
full_observability (bool): Whether the mobile agents' spatial observation
|
||||
includes the full world view or is instead an egocentric view.
|
||||
mobile_agent_observation_range (int): If not using full_observability,
|
||||
the spatial range (on each side of the agent) that is visible in the
|
||||
spatial observations.
|
||||
env_layout_file (str): Name of the layout file in ./map_txt/ to use.
|
||||
Note: The world dimensions of that layout must match the world dimensions
|
||||
argument used to construct the environment.
|
||||
resource_regen_prob (float): Probability that an empty source tile will
|
||||
regenerate a new resource unit.
|
||||
starting_agent_coin (int, float): Amount of coin agents have at t=0. Defaults
|
||||
to zero coin.
|
||||
isoelastic_eta (float): Parameter controlling the shape of agent utility
|
||||
wrt coin endowment.
|
||||
energy_cost (float): Coefficient for converting labor to negative utility.
|
||||
energy_warmup_constant (float): Decay constant that controls the rate at which
|
||||
the effective energy cost is annealed from 0 to energy_cost. Set to 0
|
||||
(default) to disable annealing, meaning that the effective energy cost is
|
||||
always energy_cost. The units of the decay constant depend on the choice of
|
||||
energy_warmup_method.
|
||||
energy_warmup_method (str): How to schedule energy annealing (warmup). If
|
||||
"decay" (default), use the number of completed episodes. If "auto",
|
||||
use the number of timesteps where the average agent reward was positive.
|
||||
planner_reward_type (str): The type of reward used for the planner. Options
|
||||
are "coin_eq_times_productivity" (default),
|
||||
"inv_income_weighted_coin_endowment", and "inv_income_weighted_utility".
|
||||
mixing_weight_gini_vs_coin (float): Degree to which equality is ignored w/
|
||||
"coin_eq_times_productivity". Default is 0, which weights equality and
|
||||
productivity equally. If set to 1, only productivity is rewarded.
|
||||
"""
|
||||
|
||||
name = "split_layout/simple_wood_and_stone"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
water_row=None,
|
||||
skill_rank_of_top_agents=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.fixed_four_skill_and_loc:
|
||||
raise ValueError(
|
||||
"The split layout scenario does not support "
|
||||
"fixed_four_skill_and_loc. Set this to False."
|
||||
)
|
||||
|
||||
# Augment the fixed layout to include a row of water through the middle
|
||||
if water_row is None:
|
||||
self._water_line = self.world_size[0] // 2
|
||||
else:
|
||||
self._water_line = int(water_row)
|
||||
assert 0 < self._water_line < self.world_size[0] - 1
|
||||
for landmark, landmark_map in self._source_maps.items():
|
||||
landmark_map[self._water_line, :] = 1 if landmark == "Water" else 0
|
||||
self._source_maps[landmark] = landmark_map
|
||||
|
||||
# Controls logic for which agents (by skill rank) get placed on the top
|
||||
if skill_rank_of_top_agents is None:
|
||||
skill_rank_of_top_agents = [0]
|
||||
|
||||
if isinstance(skill_rank_of_top_agents, (int, float)):
|
||||
self.skill_rank_of_top_agents = [int(skill_rank_of_top_agents)]
|
||||
elif isinstance(skill_rank_of_top_agents, (tuple, list)):
|
||||
self.skill_rank_of_top_agents = list(set(skill_rank_of_top_agents))
|
||||
else:
|
||||
raise TypeError(
|
||||
"skill_rank_of_top_agents must be a scalar "
|
||||
"index, or a list of scalar indices."
|
||||
)
|
||||
for rank in self.skill_rank_of_top_agents:
|
||||
assert 0 <= rank < self.n_agents
|
||||
assert 0 < len(self.skill_rank_of_top_agents) < self.n_agents
|
||||
|
||||
# Set the skill associated with each skill rank
|
||||
bm = self.get_component("Build")
|
||||
assert bm.skill_dist == "pareto"
|
||||
pmsm = bm.payment_max_skill_multiplier
|
||||
# Generate a batch (100000) of num_agents (sorted/clipped) Pareto samples.
|
||||
pareto_samples = np.random.pareto(4, size=(100000, self.n_agents))
|
||||
clipped_skills = np.minimum(pmsm, (pmsm - 1) * pareto_samples + 1)
|
||||
sorted_clipped_skills = np.sort(clipped_skills, axis=1)
|
||||
# The skill level of the i-th skill-ranked agent is the average of the
|
||||
# i-th ranked samples throughout the batch.
|
||||
average_ranked_skills = sorted_clipped_skills.mean(axis=0)
|
||||
self._avg_ranked_skill = average_ranked_skills * bm.payment
|
||||
# Reverse the order so index 0 is the highest-skilled
|
||||
self._avg_ranked_skill = self._avg_ranked_skill[::-1]
|
||||
|
||||
def additional_reset_steps(self):
|
||||
"""
|
||||
Extra scenario-specific steps that should be performed at the end of the reset
|
||||
cycle.
|
||||
|
||||
For each reset cycle...
|
||||
First, reset_starting_layout() and reset_agent_states() will be called.
|
||||
|
||||
Second, <component>.reset() will be called for each registered component.
|
||||
|
||||
Lastly, this method will be called to allow for any final customization of
|
||||
the reset cycle.
|
||||
|
||||
For this scenario, this method resets optimization metric trackers. This is
|
||||
where each agent gets assigned to one of the skills and the starting
|
||||
locations are reset according to self.skill_rank_of_top_agents.
|
||||
"""
|
||||
self.world.clear_agent_locs()
|
||||
for i, agent in enumerate(self.world.get_random_order_agents()):
|
||||
agent.state["build_payment"] = self._avg_ranked_skill[i]
|
||||
if i in self.skill_rank_of_top_agents:
|
||||
r_min, r_max = 0, self._water_line
|
||||
else:
|
||||
r_min, r_max = self._water_line + 1, self.world_size[0]
|
||||
|
||||
r = np.random.randint(r_min, r_max)
|
||||
c = np.random.randint(0, self.world_size[1])
|
||||
n_tries = 0
|
||||
while not self.world.can_agent_occupy(r, c, agent):
|
||||
r = np.random.randint(r_min, r_max)
|
||||
c = np.random.randint(0, self.world_size[1])
|
||||
n_tries += 1
|
||||
if n_tries > 200:
|
||||
raise TimeoutError
|
||||
self.world.set_agent_loc(agent, r, c)
|
||||
|
||||
# compute current objectives
|
||||
curr_optimization_metric = self.get_current_optimization_metrics()
|
||||
|
||||
self.curr_optimization_metric = deepcopy(curr_optimization_metric)
|
||||
self.init_optimization_metric = deepcopy(curr_optimization_metric)
|
||||
self.prev_optimization_metric = deepcopy(curr_optimization_metric)
|
||||
+1
@@ -0,0 +1 @@
|
||||
WWWWW W @ W ;WW W @ W W WW; W @ W W;SW S S @ ; @ W W ; SS @ ; S @ ; @ ; @ ;S S @ ; @ ; @ ;@@@@@@@@@@@@@@@@@@@@@@@@@; S @ ;S S @ ; S @ ;SS @ ; SS @ ;SS @ ; S @ ; @ ; @ ; @ ; @ ;S @ ;
|
||||
+1
@@ -0,0 +1 @@
|
||||
WWW @ ;WSS @ ;WWW @ ;WWW @ ;WSS ;SWS ; @ ;@@@@ @@@@ @@@; @ ;WWW @ S ; WW SS ;WWW SS;W W @ ;W @ ; @ S ;
|
||||
+1
@@ -0,0 +1 @@
|
||||
WW W @ ; SWWW @ ;SSWW @ ;WSSSW @ ;WSSWW ;WS WS ; WWS ;SWW S @ ; S W @ ; WS W @ ; @ ; @ ;@@@@ @@@@@@@@@@@@ @@@; @ ; @ ; W @ SSSSS; WW @ SSS ; @ S SS; WW W @ SSS S;W WW SSSS ;WWW S SS; WWWW S S ;WW W @ S ; W @ S ; W @ S SSS;
|
||||
+1
@@ -0,0 +1 @@
|
||||
W WW ; W ; W W W ; W ; W W ; W W W ; W W ; WW ; ; ; ; ; ; ; ; ; SS W ; WW W; ; S S S; W WW S ; S S W ; W WS S S ; ; ; ; ; ; ; ; ; S ;S S ; ; SS ; ; S SS S ; SS S S ;S SS S S ;SSSS S SS ;
|
||||
+1
@@ -0,0 +1 @@
|
||||
WWWWW W @ W ;WW W @ W W WW; W @ W W;SW S S @ ; @ W W ; SS ; S ; ; ;S S @ ; @ ; @ ;@@@@@ @@@@@@@ @@@@@; S @ ;S S @ ; S @ ;SS ; SS ;SS ; S ; @ ; @ ; @ ; @ ;S @ ;
|
||||
+1
@@ -0,0 +1 @@
|
||||
WWWWW W W ;WW W W W WW; W W W;SW S S ; W W ; SS ; S ; ; ;S S ; ; ; ; S ;S S ; S ;SS ; SS ;SS ; S ; ; ; ; ;S ;
|
||||
+1
@@ -0,0 +1 @@
|
||||
WWWWWWWW WW @@ WW ;WWWWWWWW WW @@ WW ;WWW W @@ WW W WWW; W @@ WW WW; W @@ WW WW;SSW S SS @@ ; @@ W W ; @@ W W ; SSS ; SSS ; S ; ; ; ;SS SS @@ ;SS SS @@ ; @@ ; @@ ; @@ ;@@@@@@@@ @@@@@@@@@@@@ @@@@@@@@;@@@@@@@@ @@@@@@@@@@@@ @@@@@@@@; S @@ ;SS SS @@ ;SS SS @@ ; S @@ ; S @@ ;SSS ; SSS ; SSS ;SSS ; SS ; SS ; @@ ; @@ ; @@ ; @@ ; @@ ; @@ ;SS @@ ;SS @@ ;
|
||||
+1
@@ -0,0 +1 @@
|
||||
WWWWWWWW WW WW ;WWWWWWWW WW WW ;WWW W WW W WWW; W WW WW; W WW WW;SSW S SS ; W W ; W W ; SSS ; SSS ; S ; ; ; ;SS SS ;SS SS ; ; ; ; ; ; S ;SS SS ;SS SS ; S ; S ;SSS ; SSS ; SSS ;SSS ; SS ; SS ; ; ; ; ; ; ;SS ;SS ;
|
||||
+1
@@ -0,0 +1 @@
|
||||
WWWW@WW W; WW @ WWW;SW S@ S ;S @ ;@@ @@@ @@; S @ ;S S @ ;S @ ; S@ ;
|
||||
+1
@@ -0,0 +1 @@
|
||||
WW W ; A WW A ;WW WA W ; A A ; ; AA A ; A ; ; ; ; A ; A SS S;SA S AS;S SA SA
|
||||
+1
@@ -0,0 +1 @@
|
||||
SSSSS; SS SSSS; SS SSSS; S SSSS; SS ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; W; W WW; WW ;W WW W W WWW;WWWWW WW W WWWWW;
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) 2020, salesforce.com, inc.
|
||||
# All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# For full license text, see the LICENSE file in the repo root
|
||||
# or https://opensource.org/licenses/BSD-3-Clause
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2020, salesforce.com, inc.
|
||||
# All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# For full license text, see the LICENSE file in the repo root
|
||||
# or https://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ai_economist.foundation.scenarios.utils import social_metrics
|
||||
|
||||
|
||||
def isoelastic_coin_minus_labor(
|
||||
coin_endowment, total_labor, isoelastic_eta, labor_coefficient
|
||||
):
|
||||
"""Agent utility, concave increasing in coin and linearly decreasing in labor.
|
||||
|
||||
Args:
|
||||
coin_endowment (float, ndarray): The amount of coin owned by the agent(s).
|
||||
total_labor (float, ndarray): The amount of labor performed by the agent(s).
|
||||
isoelastic_eta (float): Constant describing the shape of the utility profile
|
||||
with respect to coin endowment. Must be between 0 and 1. 0 yields utility
|
||||
that increases linearly with coin. 1 yields utility that increases with
|
||||
log(coin). Utility from coin uses:
|
||||
https://en.wikipedia.org/wiki/Isoelastic_utility
|
||||
labor_coefficient (float): Constant describing the disutility experienced per
|
||||
unit of labor performed. Disutility from labor equals:
|
||||
labor_coefficient * total_labor
|
||||
|
||||
Returns:
|
||||
Agent utility (float) or utilities (ndarray).
|
||||
"""
|
||||
# https://en.wikipedia.org/wiki/Isoelastic_utility
|
||||
assert np.all(coin_endowment >= 0)
|
||||
assert 0 <= isoelastic_eta <= 1.0
|
||||
|
||||
# Utility from coin endowment
|
||||
if isoelastic_eta == 1.0: # dangerous
|
||||
util_c = np.log(np.max(1, coin_endowment))
|
||||
else: # isoelastic_eta >= 0
|
||||
util_c = (coin_endowment ** (1 - isoelastic_eta) - 1) / (1 - isoelastic_eta)
|
||||
|
||||
# disutility from labor
|
||||
util_l = total_labor * labor_coefficient
|
||||
|
||||
# Net utility
|
||||
util = util_c - util_l
|
||||
|
||||
return util
|
||||
|
||||
|
||||
def coin_minus_labor_cost(
|
||||
coin_endowment, total_labor, labor_exponent, labor_coefficient
|
||||
):
|
||||
"""Agent utility, linearly increasing in coin and decreasing as a power of labor.
|
||||
|
||||
Args:
|
||||
coin_endowment (float, ndarray): The amount of coin owned by the agent(s).
|
||||
total_labor (float, ndarray): The amount of labor performed by the agent(s).
|
||||
labor_exponent (float): Constant describing the shape of the utility profile
|
||||
with respect to total labor. Must be between >1.
|
||||
labor_coefficient (float): Constant describing the disutility experienced per
|
||||
unit of labor performed. Disutility from labor equals:
|
||||
labor_coefficient * total_labor.
|
||||
|
||||
Returns:
|
||||
Agent utility (float) or utilities (ndarray).
|
||||
"""
|
||||
# https://en.wikipedia.org/wiki/Isoelastic_utility
|
||||
assert np.all(coin_endowment >= 0)
|
||||
assert labor_exponent > 1
|
||||
|
||||
# Utility from coin endowment
|
||||
util_c = coin_endowment
|
||||
|
||||
# Disutility from labor
|
||||
util_l = (total_labor ** labor_exponent) * labor_coefficient
|
||||
|
||||
# Net utility
|
||||
util = util_c - util_l
|
||||
|
||||
return util
|
||||
|
||||
|
||||
def coin_eq_times_productivity(coin_endowments, equality_weight):
|
||||
"""Social welfare, measured as productivity scaled by the degree of coin equality.
|
||||
|
||||
Args:
|
||||
coin_endowments (ndarray): The array of coin endowments for each of the
|
||||
agents in the simulated economy.
|
||||
equality_weight (float): Constant that determines how productivity is scaled
|
||||
by coin equality. Must be between 0 (SW = prod) and 1 (SW = prod * eq).
|
||||
|
||||
Returns:
|
||||
Product of coin equality and productivity (float).
|
||||
"""
|
||||
n_agents = len(coin_endowments)
|
||||
prod = social_metrics.get_productivity(coin_endowments) / n_agents
|
||||
equality = equality_weight * social_metrics.get_equality(coin_endowments) + (
|
||||
1 - equality_weight
|
||||
)
|
||||
return equality * prod
|
||||
|
||||
|
||||
def inv_income_weighted_coin_endowments(coin_endowments):
|
||||
"""Social welfare, as weighted average endowment (weighted by inverse endowment).
|
||||
|
||||
Args:
|
||||
coin_endowments (ndarray): The array of coin endowments for each of the
|
||||
agents in the simulated economy.
|
||||
|
||||
Returns:
|
||||
Weighted average coin endowment (float).
|
||||
"""
|
||||
pareto_weights = 1 / np.maximum(coin_endowments, 1)
|
||||
pareto_weights = pareto_weights / np.sum(pareto_weights)
|
||||
return np.sum(coin_endowments * pareto_weights)
|
||||
|
||||
|
||||
def inv_income_weighted_utility(coin_endowments, utilities):
|
||||
"""Social welfare, as weighted average utility (weighted by inverse endowment).
|
||||
|
||||
Args:
|
||||
coin_endowments (ndarray): The array of coin endowments for each of the
|
||||
agents in the simulated economy.
|
||||
utilities (ndarray): The array of utilities for each of the agents in the
|
||||
simulated economy.
|
||||
|
||||
Returns:
|
||||
Weighted average utility (float).
|
||||
"""
|
||||
pareto_weights = 1 / np.maximum(coin_endowments, 1)
|
||||
pareto_weights = pareto_weights / np.sum(pareto_weights)
|
||||
return np.sum(utilities * pareto_weights)
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright (c) 2020, salesforce.com, inc.
|
||||
# All rights reserved.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# For full license text, see the LICENSE file in the repo root
|
||||
# or https://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def get_gini(endowments):
|
||||
"""Returns the normalized Gini index describing the distribution of endowments.
|
||||
|
||||
https://en.wikipedia.org/wiki/Gini_coefficient
|
||||
|
||||
Args:
|
||||
endowments (ndarray): The array of endowments for each of the agents in the
|
||||
simulated economy.
|
||||
|
||||
Returns:
|
||||
Normalized Gini index for the distribution of endowments (float). A value of 1
|
||||
indicates everything belongs to 1 agent (perfect inequality), whereas a
|
||||
value of 0 indicates all agents have equal endowments (perfect equality).
|
||||
|
||||
Note:
|
||||
Uses a slightly different method depending on the number of agents. For fewer
|
||||
agents (<30), uses an exact but slow method. Switches to using a much faster
|
||||
method for more agents, where both methods produce approximately equivalent
|
||||
results.
|
||||
"""
|
||||
n_agents = len(endowments)
|
||||
|
||||
if n_agents < 30: # Slower. Accurate for all n.
|
||||
diff_ij = np.abs(
|
||||
endowments.reshape((n_agents, 1)) - endowments.reshape((1, n_agents))
|
||||
)
|
||||
diff = np.sum(diff_ij)
|
||||
norm = 2 * n_agents * endowments.sum(axis=0)
|
||||
unscaled_gini = diff / (norm + 1e-10)
|
||||
gini = unscaled_gini / ((n_agents - 1) / n_agents)
|
||||
return gini
|
||||
|
||||
# Much faster. Slightly overestimated for low n.
|
||||
s_endows = np.sort(endowments)
|
||||
return 1 - (2 / (n_agents + 1)) * np.sum(
|
||||
np.cumsum(s_endows) / (np.sum(s_endows) + 1e-10)
|
||||
)
|
||||
|
||||
|
||||
def get_equality(endowments):
|
||||
"""Returns the complement of the normalized Gini index (equality = 1 - Gini).
|
||||
|
||||
Args:
|
||||
endowments (ndarray): The array of endowments for each of the agents in the
|
||||
simulated economy.
|
||||
|
||||
Returns:
|
||||
Normalized equality index for the distribution of endowments (float). A value
|
||||
of 0 indicates everything belongs to 1 agent (perfect inequality),
|
||||
whereas a value of 1 indicates all agents have equal endowments (perfect
|
||||
equality).
|
||||
"""
|
||||
return 1 - get_gini(endowments)
|
||||
|
||||
|
||||
def get_productivity(coin_endowments):
|
||||
"""Returns the total coin inside the simulated economy.
|
||||
|
||||
Args:
|
||||
coin_endowments (ndarray): The array of coin endowments for each of the
|
||||
agents in the simulated economy.
|
||||
|
||||
Returns:
|
||||
Total coin endowment (float).
|
||||
"""
|
||||
return np.sum(coin_endowments)
|
||||
Reference in New Issue
Block a user