Commit 7cfaff91 authored by Jason Rhinelander's avatar Jason Rhinelander

Initial pyeris code

This builds eris.core (included by importing 'eris') containing the
basic pyeris members, and eris.position with the position and
positional code.  It supports optimization as done in eris, but by
simplying defining a method of the appropriate name rather than via
inheritance.

Beliefs, randomization, and the simple implementations (of firms,
markets, consumers, etc.) are not yet included.
parent a89ef9e9
[submodule "pybind11"]
path = pybind11
url = https://github.com/pybind/pybind11.git
branch = master
cmake_minimum_required(VERSION 2.8.8)
project(pyeris CXX)
include(GNUInstallDirs)
set(pyeris_author "Jason Rhinelander <jason@imaginary.ca>")
set(pyeris_url "https://git.imaginary.ca/eris/pyeris")
set(pyeris_description "Agent-based economic modelling library - python interface")
# pyeris package version
set(PYERIS_VERSION_MAJOR "0")
set(PYERIS_VERSION_MINOR "0")
set(PYERIS_VERSION_PATCH "0")
set(PYERIS_VERSION "${PYERIS_VERSION_MAJOR}.${PYERIS_VERSION_MINOR}.${PYERIS_VERSION_PATCH}")
# pyeris library version (CURRENT.REVISION.AGE), which is totally separate
# from the above. See http://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html
# Basic rules for updating these when releasing a new pyeris version:
# - If the new release has an API change:
# - CURRENT gets updated
# - REVISION gets set to 0
# - If the API change was purely an addition, increment AGE
# - Otherwise (i.e. an API change or removal), reset AGE to 0
# - Otherwise, if the release has no API change but has code changes:
# - REVISION gets incremented
# - (CURRENT and AGE stay the same)
# If there is no code change (e.g. the release is just a documentation update)
# then none of these change.
#
# (So something like 3.7.1 indicates the 8th revision of the libpyeris.so.3
# interface, and that code that links against libpyeris.so.2 can safely link
# against this version, but code that links against libpyeris.so.1 cannot.
set(LIBPYERIS_CURRENT "0")
set(LIBPYERIS_REVISION "0")
set(LIBPYERIS_AGE "0")
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules)
set(ERIS_VERSION_REQUIRED "0.5.0")
# No in-source building
include(MacroEnsureOutOfSourceBuild)
macro_ensure_out_of_source_build("${PROJECT_NAME} requires an out-of-source build. Create a build directory and run 'cmake ${CMAKE_SOURCE_DIR} [options]'.")
set(CMAKE_CXX_FLAGS "-std=c++11 -O2 -Wall -Wextra ${CMAKE_CXX_FLAGS}")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -DERIS_DEBUG")
# Make sure pybind11 exists
if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/pybind11/CMakeLists.txt")
add_subdirectory(pybind11)
else()
message(FATAL_ERROR "pybind11/CMakeLists.txt does not exist; perhaps you need to run `git submodule update'?")
endif()
# Eigen: Move semantics were during 3.3 development, and backported in 3.2.7:
find_package(Eigen3 REQUIRED)
if (EIGEN3_VERSION_STRING VERSION_LESS "3.2.7")
message(FATAL_ERROR "Eigen v3.2.7 required, but found v${EIGEN3_VERSION_STRING}")
else()
message(STATUS "Eigen version: ${EIGEN3_VERSION_STRING}")
endif()
include_directories(${EIGEN3_INCLUDE_DIR})
# Python libs
#find_package(PythonLibs REQUIRED)
#include_directories(${PYTHON_INCLUDE_DIRS})
#set(BOOST_MIN 1.46.0)
#find_package(Boost ${BOOST_MIN} REQUIRED)
# The boost python library can live under a few different names: on Debian/Ubuntu, it seems to be
# named libboost_python-pyXY.so where XY are the major/minor python version (e.g. 35 for 3.5.z).
# Some distributions instead name it libboost_pythonX.so, where X is the major version. The code
# below tries the following, where $V is the major and $v is the minor version, and uses the first one found:
# libboost_python-py$V.$v.so
# libboost_python-$V.$v.so
# libboost_python$V.$v.so
# libboost_python-py$V$v.so
# libboost_python-$V$v.so
# libboost_python$V$v.so
# libboost_python-py$V.so
# libboost_python-$V.so
# libboost_python$V.so
# libboost_python.so
# If none are found, we give up.
#string(REGEX MATCH "^[0-9]+" boost_py_v "${PYTHONLIBS_VERSION_STRING}")
#string(REGEX MATCH "^[0-9]+\\.[0-9]+" boost_py_vdotmin "${PYTHONLIBS_VERSION_STRING}")
#string(REGEX REPLACE "\\." "" boost_py_vmin "${boost_py_vdotmin}")
#if ("${boost_py_v}" STREQUAL "" OR "${boost_py_vdotmin}" STREQUAL "" OR "${boost_py_vmin}" STREQUAL "")
# message(FATAL "Could not determine python library major/minor version: ${PYTHONLIBS_VERSION_STRING}")
#endif()
#foreach (v "${boost_py_vdotmin}" "${boost_py_vmin}" "${boost_py_v}")
# foreach (suffix "-py" "-" "")
# string(TOUPPER "${suffix}${v}" suffv_uc)
# message(STATUS "Checking for boost_python${suffix}${v}")
# find_package(Boost ${BOOST_MIN} COMPONENTS "python${suffix}${v}")
# set(Boost_PYTHON_FOUND ${Boost_PYTHON${suffv_uc}_FOUND})
# if (Boost_PYTHON_FOUND)
# break()
# endif()
# endforeach()
# if (Boost_PYTHON_FOUND)
# break()
# endif()
#endforeach()
#
## Lastly look for libboost_python.so: make this REQUIRED so that it fails if not found
#if (NOT Boost_PYTHON_FOUND)
# message(STATUS "Checking for boost_python")
# find_package(Boost ${BOOST_MIN} REQUIRED COMPONENTS python)
#endif()
#
#include_directories(${Boost_INCLUDE_DIRS})
#link_directories(${Boost_LIBRARY_DIRS})
find_package(PkgConfig REQUIRED)
pkg_check_modules(ERIS REQUIRED liberis)
if(ERIS_VERSION VERSION_LESS "${ERIS_VERSION_REQUIRED}")
message(FATAL_ERROR "Eris v${ERIS_VERSION_REQUIRED} or above is required, but only v${ERIS_VERSION} was found")
endif()
include_directories(${ERIS_INCLUDE_DIRS})
link_directories(${ERIS_LIBRARY_DIRS})
add_definitions(${ERIS_CFLAGS_OTHER})
file(GLOB core_src RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} pyeris/core.cpp pyeris/core/*.cpp)
file(GLOB pos_src RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} pyeris/position.cpp pyeris/position/*.cpp)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/package/eris DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/eris)
pybind11_add_module(core ${core_src})
target_link_libraries(core ${ERIS_LIBRARIES})
set_target_properties(core PROPERTIES OUTPUT_NAME eris/core)
pybind11_add_module(position ${pos_src})
target_link_libraries(position ${ERIS_LIBRARIES})
set_target_properties(position PROPERTIES OUTPUT_NAME eris/position)
# Make libpyeris.pc for the 'make install' target
#configure_file(${CMAKE_CURRENT_SOURCE_DIR}/libpyeris.pc.in
# ${CMAKE_CURRENT_BINARY_DIR}/libpyeris.pc
# @ONLY)
#
#install(TARGETS eris
# DESTINATION "${CMAKE_INSTALL_LIBDIR}")
#install(FILES ${CMAKE_CURRENT_BINARY_DIR}/liberis.pc
# DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
#install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/eris/"
# DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/eris/"
# FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h")
#install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/eris/"
# DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/eris/"
# FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h")
#
#install(FILES README.md LICENSE DESTINATION "${CMAKE_INSTALL_DOCDIR}")
......@@ -14,18 +14,11 @@ C++ interface.
## License
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
This program is licensed under the GNU General Public License, version 3 or
later; see [LICENSE] for details.
This program also makes use of the pybind11 package, distributed under a BSD
3-clause license; see pybind11/LICENSE for details.
## Author
......
# - MACRO_ENSURE_OUT_OF_SOURCE_BUILD(<errorMessage>)
# MACRO_ENSURE_OUT_OF_SOURCE_BUILD(<errorMessage>)
# Copyright (c) 2006, Alexander Neundorf, <neundorf@kde.org>
#
# Redistribution and use is allowed according to the terms of the BSD license:
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
macro (MACRO_ENSURE_OUT_OF_SOURCE_BUILD _errorMessage)
string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BINARY_DIR}" _insource)
if (_insource)
message(SEND_ERROR "${_errorMessage}")
message(FATAL_ERROR "Remove the file CMakeCache.txt in ${CMAKE_SOURCE_DIR} first.")
endif (_insource)
endmacro (MACRO_ENSURE_OUT_OF_SOURCE_BUILD)
from eris.core import *
Subproject commit 2353b9b8fa3dfc91b50b8ba9ddaa41fe166c5360
#pragma once
#include <iostream> // debug
#include <pybind11/pybind11.h>
#include <eris/SharedMember.hpp>
#include <eris/types.hpp>
namespace py = pybind11;
using namespace eris;
using namespace pybind11::literals;
PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr<T>);
PYBIND11_DECLARE_HOLDER_TYPE(T, eris::SharedMember<T>);
#include "pyeris/common.hpp"
#include "pyeris/core/Simulation.hpp"
#include "pyeris/core/bundles.hpp"
#include "pyeris/core/members.hpp"
// The core eris classes: Simulation, SharedMember, Member, Agent, Good, Market, Bundle, Optimize
namespace pyeris {
PYBIND11_PLUGIN(core) {
py::module m("eris", "eris interface for Python -- core functionality");
core::bind_simulation(m);
core::bind_bundles(m);
core::bind_members(m);
// Expose the individual inter/intra-optimizer interfaces. pybind11 doesn't handle C++ multiple
// inheritance, but that's okay: we can do the multiple inheritance on the python side
// (inheriting from both Member and one or more of these).
//py::class<interopt::Begin, std::shared_ptr<Inter
return m.ptr();
}
}
#include "pyeris/core/Firm.hpp"
namespace pyeris { namespace core {
void bind_firm(py::module &m, py::class_<Agent, std::shared_ptr<Agent>> &agent) {
py::class_<Firm, std::shared_ptr<Firm>, PyFirm> firm(m, "Firm", agent);
py::class_<Firm::Reservation> reservation(firm, "Reservation");
reservation
.def_readonly("state", &Firm::Reservation::state, "The state of this firm-level reservation; equal to one of ReservationState.pending, ReservationState.completed, or ReservationState.aborted")
.def_readonly("bundle", &Firm::Reservation::bundle, "The bundle that is reserved. When the reservation is completed, positive quantities are transferred out of the firm and negative quantities are transferred into the firm")
.def_readonly("firm", &Firm::Reservation::firm, "The firm for which this reservation applies")
.def("transfer", &Firm::Reservation::transfer, "buyer"_a, "Completes the reservation by transferring the reserved assets from the firm to the given buyer assets bundle. Negative transfer amounts are transferred from the buyer's assets to the firm's assets.")
.def("release", &Firm::Reservation::release, "Aborts a firm-level reservation; no transfer takes place, and the firm reserved assets and/or production are updated")
;
firm.def("can_supply", &Firm::canSupply, "bundle"_a, "Returns True if the firm can supply the given bundle; the default implementation simply returns True if can_supply_any() >= 1.0")
.def("can_supply_any", &Firm::canSupplyAny, "bundle"_a, "Returns a floating point value indicating the fraction of the requested bundle that the firm is able to supply. The default considers both the firm's on-hand assets and the result of the _can_produce_any method. Returns a value >= 1 if the firm can supply the entire bundle. Subclasses may return a value of 1.0 even if larger multiples can be supplied")
.def("supplies", &Firm::supplies, "bundle"_a, "This is essentially the same as calling `can_supply_any() > 0`, but is more efficient when the precise supply multiple is not needed")
.def("reserve", &Firm::reserve, "reserve"_a, "Reserves the given quantities to be later transferred from the firm (or to the firm, for negative quantities) by a call of transfer() on the returned object, or aborted by a call of release(). If the firm can supply some or all of the bundle from its on-hand assets, it is removed and placed in a special reserved assets bundle; the remainder is reserved by an internal call to the firm's _produce method.")
.def("supply", &Firm::supply, "bundle"_a, "assets"_a, "Supplies the given quantities immediately; this is equivalent to calling `firm.reserve(bundle).transfer(assets)`. Returns the (completed) Reservation")
.def_readwrite("epsilon", &Firm::epsilon, "The relative tolerance by which to adjust transfers; negative adjustments can be made to satisfy constraints; positive adjustments can be made to avoid leaving nearly-0 amounts behind. The default is 1e-10")
.def("_create_reservation", &PyFirm::_create_reservation, "bundle"_a, "Creates a Firm.Reservation object and returns it. For Firm subclasses only: external callers should use reserve() instead")
.def("_can_produce", &PyFirm::canProduce, "bundle"_a, "Returns True if the firm is able to produce the requested bundle. Default implementation simply returns `firm._can_produce_any(b) >= 1`")
.def("_can_produce_any", &PyFirm::canProduceAny, "bundle"_a, "Abstract method that must be provided by a Firm subclass. Analogous to (and called by) can_supply_any(), this method returns a value in [0, 1] indicating the multiple of the bundle that the firm is able to produce instantly if needed. Subclasses may, but are not required, to return a value > 1 when more than the requested bundle quantities can be produced.")
.def("_produces", &PyFirm::produces, "bundle"_a, "Returns true if the firm is able to produce some positive multiple of the requested bundle. Equivalent to `firm._can_produce_any(bundle) > 0`, but may be more efficient when the precise multiple is not needed")
.def("_reserve_production", &PyFirm::reserveProduction, "bundle"_a, "Abstract method: must be implemented by Firm implementations. Reserves the given bundle to be produced by a subsequent _produce call. Can throw a FirmSupplyFailure exception (or exception subclass) if production is impossible. Otherwise, this method must increase _reserved_production by the requested output bundle; if production of that bundle will result in any excess production, the excess quantities should be added to _excess_production")
.def("_produce", &PyFirm::produce, "bundle"_a, "Abstract method called when completing a reservation that requires production to satisfy some or all of the required output. `bundle` is the output required; returns the produced bundle that must be >= `bundle`. This method should not update _reserved_production, _excess_production, or the firm's assets: those will be updated appropriately based on the produced bundle")
.def("_reduce_excess_production", &PyFirm::reduceExcessProduction, "Abstract method called after cancelling a reserved transfer. This is essentially the opposite of reserve_production_: it should reduce any reserved production output as much as possible according to the excess planned production currently in excess_production_. For example, supposed a firm produces multiples of Bundle({ good1: 1, good2: 4 }) units, and has an internal variable indicating that 30 units may still be reserved for production before hitting a capacity constraint; if this is called with _excess_production containing 5 units each of good1 and good2, the firm would increase its available capacity by 1.25 and subtract Bundle({ good1 : 1.25, good2 : 5 }) from _excess_production.")
.def_readwrite("_reserves", &PyFirm::_reserves, py::return_value_policy::reference_internal, "This contains quantities removed from the assets() bundle that are covered by currently-pending reservations.")
.def_readwrite("_reserved_production", &PyFirm::_reserved_production, py::return_value_policy::reference_internal, "This contains quantities scheduled for production (as a result of currently-pending reservations) that will be produced when current reservations are completed.")
.def_readwrite("_excess_production", &PyFirm::_excess_production, py::return_value_policy::reference_internal, "This contains the amounts that will be produced to satisfy reserved production (due to reservations) but that is not actually part of the transferred quantity. Typically this results from a multiple-good production where the requested output units are not a perfect multiple of the production unit")
;
py::class_<FirmNoProd, std::shared_ptr<FirmNoProd>, PyFirmNoProd> firm_noprod(m, "FirmNoProd", firm);
firm_noprod
.def("ensure_next", &FirmNoProd::ensureNext, "bundle"_a, "Can be called during an inter-period optimization stage to ensure that (at least) the requested bundle will be available in the subsequent period. This calls _product_next with any quantity that isn't satisfiable from the firm's current assets()")
.def("_produce_next", &FirmNoProd::produceNext, "bundle"_a, "Called internally by ensure_next with the amount that needs to be produced and added to assets() to satisfy the required bundle amount. This abstract method must be provided by a subclass.")
;
py::register_exception<Firm::Reservation::non_pending_exception>(m, "FirmReservationNonPending");
// General exception for supply failures:
auto supply_ex = py::register_exception<Firm::supply_failure>(m, "FirmSupplyFailure");
py::register_exception<Firm::supply_mismatch>(m, "FirmSupplyMismatch", supply_ex.ptr());
auto pc_ex = py::register_exception<Firm::production_constraint>(m, "FirmProductionConstraint", supply_ex.ptr());
py::register_exception<Firm::production_unavailable>(m, "FirmProductionUnavailable", pc_ex.ptr());
py::register_exception<Firm::production_unreserved>(m, "FirmProductionUnreserved", supply_ex.ptr());
}
}}
#pragma once
#include "pyeris/common.hpp"
#include <eris/Firm.hpp>
using namespace eris::agent;
namespace pyeris { namespace core {
// Trampoline class for (abstract) Firm
class PyFirm : public eris::Firm {
public:
// Inherit constructors
using Firm::Firm;
// Virtual functions (these must have the same name, since they are overrides):
bool canSupply(const Bundle &b) const override { PYBIND11_OVERLOAD_NAME(bool, Firm, "can_supply", canSupply, b); }
double canSupplyAny(const Bundle &b) const override { PYBIND11_OVERLOAD_NAME(double, Firm, "can_supply_any", canSupplyAny, b); }
bool supplies(const Bundle &b) const override { PYBIND11_OVERLOAD(bool, Firm, supplies, b); }
bool canProduce(const Bundle &b) const override { PYBIND11_OVERLOAD_NAME(bool, Firm, "_can_produce", canProduce, b); }
double canProduceAny(const Bundle &b) const override { PYBIND11_OVERLOAD_NAME(double, Firm, "_can_produce_any", canProduceAny, b); }
bool produces(const Bundle &b) const override { PYBIND11_OVERLOAD_NAME(bool, Firm, "_produces", produces, b); }
Reservation reserve(const BundleNegative &reserve) override { PYBIND11_OVERLOAD(Reservation, Firm, reserve, reserve); }
Bundle produce(const Bundle &b) override { PYBIND11_OVERLOAD_PURE(Bundle, Firm, produce, b); }
void reserveProduction(const Bundle &reserve) override { PYBIND11_OVERLOAD_NAME(void, Firm, "_reserve_production", reserveProduction, reserve); }
void reduceExcessProduction() override { PYBIND11_OVERLOAD_PURE_NAME(void, Firm, "_reduce_excess_production", reduceExcessProduction); }
// These are non-virtual, but are protected and need to be exposed, but the name can differ
Reservation _create_reservation(BundleNegative bundle) { return Firm::createReservation(std::move(bundle)); }
// Access the bundles stored in the parent
Bundle* _reserves{&reserves_};
Bundle* _reserved_production{&reserved_production_};
Bundle* _excess_production{&excess_production_};
};
// Trampoline class for (abstract) FirmNoProd
class PyFirmNoProd : public PyFirm {
public:
void produceNext(const Bundle &b) { PYBIND11_OVERLOAD_PURE_NAME(void, Firm, "_produce_next", produceNext, b); }
};
void bind_firm(py::module &m, py::class_<Agent, std::shared_ptr<Agent>> &agent);
}}
#include "pyeris/core/Market.hpp"
namespace pyeris { namespace core {
void bind_market(py::module &m, py::class_<Member, std::shared_ptr<Member>, PyMember> &member) {
py::class_<Market, std::shared_ptr<Market>, PyMarket> market(m, "Market", member);
py::class_<Market::price_info> market_price(market, "PriceInfo");
market_price
.def(py::init<>(), "Constructs a default Market.PriceInfo with `feasible` set to false, and other values set to nan")
.def(py::init<double, double, double>(), " Constructs a Market.PriceInfo with `feasible` set to true and total, marginal, and marginal_first set to the given values")
.def_readwrite("feasible", &Market::price_info::feasible, "Boolean value used to indicate whether a requested quantity is available (True) or not available (False) in this market. This is the type returned by a Market's price() method")
.def_readwrite("total", &Market::price_info::feasible, "Indicates the total price (in multiple of the market's price bundle) of the requested quantity (only if `feasible` is True)")
.def_readwrite("marginal", &Market::price_info::marginal, "Contains the marginal cost per unit of the last (infinitesimal) unit of the requested quantity (only if `feasible` is True)")
.def_readwrite("marginal_first", &Market::price_info::marginalFirst, "Contains the marginal cost of the first (infinitesimal) unit of the requested quantity (only if `feasible` is True)")
;
py::class_<Market::quantity_info> market_quantity(market, "QuantityInfo");
market_quantity
.def(py::init<>(), "Constructs a default quantity info record with numeric values set to nan and `constrained` set to false")
.def(py::init<double, bool, double, double>(), "Constructs a quantity info record with the given values for quantity, constrained, spent, and unspent")
.def(py::init<double, double>(), "Constructs a quantity info record for an unconstrained quantity; the given values are set for `quantity` and `spent`; `constrained` is set to False and `unspent` is set to 0")
.def_readwrite("quantity", &Market::quantity_info::quantity, "Indicates the quantity purchasable; returned by a Market's quantity() method for a given price")
.def_readwrite("constrained", &Market::quantity_info::constrained, "If False, the quantity returned was not limited by a production constraint; if True, the requested expenditure could not be expended because of market constraints.")
.def_readwrite("spent", &Market::quantity_info::spent, "The total amount spent for the given quantity. Equals the total requested when `constrained` is false; will be less when `constrained` is true.")
.def_readwrite("unspent", &Market::quantity_info::unspent, "The amount that could not be spent because of market constraints. Will equal 0 when `constrained` is false.")
;
py::enum_<ReservationState>(m, "ReservationState")
.value("pending", ReservationState::pending)
.value("completed", ReservationState::complete)
.value("aborted", ReservationState::aborted)
;
py::class_<Market::Reservation> reservation(market, "Reservation");
reservation
.def_readonly("state", &Market::Reservation::state, "The state of this reservation; equal to one of ReservationState.pending, ReservationState.completed, or ReservationState.aborted")
.def_readonly("quantity", &Market::Reservation::quantity, "The quantity (as a multiple of the market's output_unit) of this reservation")
.def_readonly("price", &Market::Reservation::price, "The price (as a multiple of the market's price_unit) of this reservation")
.def_readonly("market", &Market::Reservation::market, "The market to which this reservation applies")
.def_readonly("agent", &Market::Reservation::agent, "The agent for which this reservation is being held")
.def("buy", &Market::Reservation::buy, "Completes this reservation, transferring the output to the purchaser and the price to the firm(s) supplying the output")
.def("release", &Market::Reservation::release, "Cancels this reservation, returning the price to the agent and releasing the reserved amount back to the participating firm(s)")
.def("firm_reserve", &Market::Reservation::firmReserve, "firm"_a, "transfer"_a,
"Specified an amount to be transferred from/to the given firm when this reservation is completed. Positive amounts are transferred from the firm to the agent (i.e. production); negative amounts are transferred from the agent to the firm (i.e. payment). Automatically calls `firm.reserve(transfer)` to have the firm reserve the requested amount. "
"If the reservation is completed (by calling reservation.buy()), the transfer amounts are completed; if cancelled (by calling reservation.release()) the reservations are released")
;
market
.def(py::init<Bundle, Bundle>(), "Base class constructor for a market; takes two bundles: the output unit (i.e. what this market sells) and the price unit (i.e. what this market takes as payment). Transactions are then carried out in terms of multiples of these base units.")
.def("price", &Market::price, "Returns the price information for buying a given quantity (as a multiple of the output bundle).")
.def("quantity", &Market::quantity, "Returns the quantity information associated with spending a certain amount (as a multiple of the price bundle).")
.def("reserve", &Market::reserve, "agent"_a, "quantity"_a, "maximum"_a = std::numeric_limits<double>::infinity(),
"Takes an agent, quantity, and maximum total price; reserves a given quantity of the market output, removing its payment from the agent and making the quantity unavailable to other agents. If the reservation is completed the quantity will be transferred to the agent's assets; if aborted, the payment will be returned. "
"Throws: MarketOutputInfeasible if q units of the output bundle are not available; MarketLowPrice if the output is available, but the cost would exceed the specified maximum; MarketInsufficientAssets if the agent's assets doesn't cover the required payment.")
.def("_create_reservation", &PyMarket::_create_reservation, "agent"_a, "quantity"_a, "maximum"_a,
"Creates a Reservation object for the given agent, quantity, and maximum expenditure. This method is for internal use by Market subclasses to actually create the object; external callers should call reserve() instead. This method removes the payment from the agent ")
.def("add_firm", &Market::addFirm, "Adds the given firm to the firms supplying output units in this market.")
.def("remove_firm", &Market::removeFirm, "Removes the given firm from the firms supplying output units in this market. This only needs to be called explicitly when the firm remains in the simulation; firms removed from the simulation will have this called automatically.")
.def("firms", &Market::firms, "Returns the set of firms that supply this market")
.def_readonly("output_unit", &Market::output_unit, "The bundle in terms of which output quantities are measured.")
.def_readonly("price_unit", &Market::price_unit, "The bundle in terms of which output prices are measured.")
;
py::register_exception<Market::output_infeasible>(m, "MarketOutputInfeasible");
py::register_exception<Market::low_price>(m, "MarketLowPrice");
py::register_exception<Market::insufficient_assets>(m, "MarketInsufficientAssets");
py::register_exception<Market::Reservation::non_pending_exception>(m, "MarketReservationNonPending");
}
}}
#pragma once
#include "pyeris/common.hpp"
#include "pyeris/core/members.hpp"
#include <eris/Market.hpp>
#include <eris/Member.hpp>
namespace pyeris { namespace core {
// Trampoline class for (abstract) Market
class PyMarket : public eris::Market {
public:
// Inherit constructors
using Market::Market;
price_info price(double q) const override { PYBIND11_OVERLOAD_PURE(price_info, Market, price, q); }
quantity_info quantity(double p) const override { PYBIND11_OVERLOAD_PURE(quantity_info, Market, quantity, p); }
// Python doesn't have move constructors, so we need a special version of this that returns a
// pointer instead of an instances. (We don't need to enforce non-copying on the Python side,
// since Python references rather than copies unless you add an explicit copying operation).
Reservation reserve(SharedMember<Agent> agent, double q, double p_max = std::numeric_limits<double>::infinity()) override {
PYBIND11_OVERLOAD_PURE(Reservation, Market, reserve, agent, q, p_max);
}
void buy(Reservation &res) override { PYBIND11_OVERLOAD(void, Market, buy, res); }
void addFirm(SharedMember<Firm> f) override { PYBIND11_OVERLOAD(void, Market, addFirm, f); }
void removeFirm(eris_id_t fid) override { PYBIND11_OVERLOAD(void, Market, removeFirm, fid); }
// Not virtual, but needs to be exposed publically
Reservation _create_reservation(const SharedMember<Agent> &agent, double q, double p) { return createReservation(agent, q, p); }
};
void bind_market(py::module &m, py::class_<Member, std::shared_ptr<Member>, PyMember> &member);
}}
#include "pyeris/core/Simulation.hpp"
#include "pyeris/core/optimizer-glue.hpp"
#include <pybind11/stl.h>
namespace pyeris { namespace core {
void bind_simulation(py::module &m) {
py::class_<Simulation, std::shared_ptr<Simulation>> simulation(m, "Simulation");
simulation
.def_static("create", &Simulation::create, "Constructs a new Simulation object")
// The C++ versions of these are templated to allow any particular Agent or Agent subclass,
// to save some type conversion, and to ensure that the requested member actually conforms
// to the given type. In Python, these are a bit looser: they always give back the generic
// Agent; the caller can verify the instance class if desired.
.def("agent", &Simulation::agent<Agent>, "Access an agent by id")
.def("good", &Simulation::good<Good>, "Access a good by id")
.def("market", &Simulation::market<Market>, "Access a market by id")
.def("other", &Simulation::other<Member>, "Access a non-agent, non-good, non-market simulation member by id")
.def("has_agent", &Simulation::hasAgent, "Returns whether the simulation has an agent with the given id")
.def("has_hood", &Simulation::hasGood, "Returns whether the simulation has a good with the given id")
.def("has_market", &Simulation::hasMarket, "Returns whether the simulation has a market with the given id")
.def("has_other", &Simulation::hasOther, "Returns whether the simulation has a non-agent/good/market member with the given id")
.def("add", &Simulation::add, "Adds a member instance to the simulation. The member is returned, to encourage usage such as: `member = sim.add(MemberObject(...))`")
// Remove can work either by id or on a Member; in eris, Member is implicitly convertible to
// an id, but pybind11 doesn't support that implicit conversion (see pybind11 issue #259),
// so we defined both for the python interface:
.def("remove", &Simulation::remove, "Removes the member with the given id (and any dependencies) from this simulation")
//.def("remove", [](Simulation &s, const Member &m) -> void { s.remove(m.id()); }, "Removes the given member (and any dependencies) from this simulation")
// Member filtering. In the C++ interface, you can filter by member sub-type (using a
// template parameter) and by arbitrary lambda function (or both). In the python interface,
// we only support filtering by lambda (the lambda can, of course, do its own subtype
// filtering).
.def("agents", [](const Simulation &s) { return s.agents(); }, "Returns a list of all simulation agents.")
.def("agents", &Simulation::agents<>, "Returns a list of simulation agents filtered by the supplied lambda.")
.def("goods", [](const Simulation &s) { return s.goods(); }, "Returns a list of all simulation goods.")
.def("goods", &Simulation::goods<>, "Returns a list of simulation goods filtered by the supplied lambda.")
.def("markets", [](const Simulation &s) { return s.markets(); }, "Returns a list of all simulation markets.")
.def("markets", &Simulation::markets<>, "Returns a list of simulation markets filtered by the supplied lambda.")
// others is a bit trickier: we want to exclude optimizer glue instances, since they are
// supposed to be hidden from the python interface (they are basically just implementation
// details of other members).
.def("others", [](const Simulation &sim) {
return sim.others<Member>([](const Member &other) -> bool {
return not dynamic_cast<const OptimizerGlue*>(&other);
});
}, "Returns a list of all non-agent/good/market members in the simulation.")
.def("others", [](const Simulation &sim, std::function<bool(const Member&)> filter) {
return sim.others<Member>([&filter](const Member &other) -> bool {
return not dynamic_cast<const OptimizerGlue*>(&other) and filter(other);
});
}, "Returns a list of non-agent/good/market members in the simulation filtered by the supplied lambda.")
.def("register_dependency", &Simulation::registerDependency, "Records a dependency between simulation members; if the depended-upon member is removed, the depending member will also be removed")
.def("register_weak_dependency", &Simulation::registerWeakDependency, "Records a weak dependency between members: unlike registerDependency, if the depended-upon member is removed, the depending member is notified rather than being removed")
.def("max_threads", (void (Simulation::*)(unsigned long)) &Simulation::maxThreads, "Sets the maximum number of threads for the next call to run()")
.def("max_threads", (unsigned long (Simulation::*)()) &Simulation::maxThreads, "Returns the maximum number of threads for the current/next run() call")
.def("run", &Simulation::run, "Runs one period of the simulation")
.def("t", &Simulation::t, "Returns the iteration number, where 1 is the first iteration.")
.def("run_stage", &Simulation::runStage, "Returns the current simulation run stage.")
.def("stage_intra", &Simulation::runStageIntra, "Returns true if the current simulation stage is one of the intra-period stages.")
.def("stage_inter", &Simulation::runStageInter, "Returns true if the current simulation stage is one of the inter-period stages.")
.def("stage_optimize", &Simulation::runStageOptimize, "Returns true if the current simulation is one of the opimization stages, i.e. inter-optimize, intra-optimize, intra-reoptimize, or intra-reset")
;
py::enum_<Simulation::RunStage>(simulation, "RunStage")
// Don't export the internal runstage enum values: idle, kill, kill_all
// inter-period stages:
.value("inter_Begin", Simulation::RunStage::inter_Begin)
.value("inter_Optimize", Simulation::RunStage::inter_Optimize)
.value("inter_Apply", Simulation::RunStage::inter_Apply)
.value("inter_Advance", Simulation::RunStage::inter_Advance)
// intra-period stages:
.value("intra_Initialize", Simulation::RunStage::intra_Initialize)
.value("intra_Reset", Simulation::RunStage::intra_Reset)
.value("intra_Optimize", Simulation::RunStage::intra_Optimize)
.value("intra_Reoptimize", Simulation::RunStage::intra_Reoptimize)
.value("intra_Apply", Simulation::RunStage::intra_Apply)
.value("intra_Finish", Simulation::RunStage::intra_Finish)
;
}
}}
#include "pyeris/common.hpp"
#include <eris/Simulation.hpp>
#include <eris/Member.hpp>
#include <eris/Agent.hpp>
#include <eris/Good.hpp>
#include <eris/Market.hpp>
#include <memory>
namespace pyeris { namespace core {
// Create the Simulation class binding and RunStage enum binding
void bind_simulation(py::module &m);
}}
This diff is collapsed.
#include "pyeris/common.hpp"
#include <eris/Bundle.hpp>
namespace pyeris { namespace core {
// Binds BundleNegative/Bundle
void bind_bundles(py::module &m);
}}
#include "pyeris/core/members.hpp"
#include "pyeris/core/optimizer-glue.hpp"
#include "pyeris/core/Market.hpp"
#include "pyeris/core/Firm.hpp"
#include <eris/Good.hpp>
#include <eris/Agent.hpp>
namespace pyeris { namespace core {
void PyMember::added() {
OptimizerGlue::applyGlue(this->simulation(), this->sharedSelf(), py::cast(this));
PYBIND11_OVERLOAD(void, Member, added);
}
void PyMember::removed() {
PYBIND11_OVERLOAD(void, Member, removed);
}
void PyMember::weakDepRemoved(eris::SharedMember<eris::Member> removed, eris_id_t old_id) {
PYBIND11_OVERLOAD(void, Member, weakDepRemoved, removed, old_id);
}
void bind_members(py::module &m) {
py::class_<Member, std::shared_ptr<Member>, PyMember> member(m, "Member");
member
.def(py::init<>())
.def("id", &Member::id, "Returns the unique (per simulation) id of this member. Returns 0 if the object is not part of a simulation. Note that a Member-derived object can also be implicitly converted to its integer id, and so can often be used directly wherever an id is required.")
.def("has_simulation", &Member::hasSimulation, "Returns true if this member belongs to a simulation, false otherwise.")
.def("simulation", (std::shared_ptr<Simulation> (Member::*)() const) &Member::simulation, "Returns the simulation this object belongs to.")
// Don't import simAgent/simGood, etc: they are useful shortcuts in C++ because of the
// templated type casting, but really aren't that useful in Python since types are dynamic
.def("depends_on", &Member::dependsOn, "Records a dependency with the member's simulation; typically invoked in the _added() method.")
.def("depends_weakly_on", &Member::dependsWeaklyOn, "Records a weak dependency with the member's simulation; typically invoked in the _added() method.")
.def("added", &PyMember::added, "Called to notify the object that it has just been added to a simulation. The simulation() and id() methods access the simulation object and the assigned id, respectively.")
.def("removed", &PyMember::removed, "Called to notify the object that it has just been removed from a simulation. The simulation() and id() methods still work until this method returns, however the simulation no longer considers the object to belong to itself.")
.def("weak_dep_removed", &PyMember::weakDepRemoved, "Called when a weak dependency of this object is removed from the simulation.")
;
py::implicitly_convertible<Member, eris_id_t>();
py::class_<Agent, std::shared_ptr<Agent>> agent(m, "Agent", member);
agent
.def(py::init<>())
.def_readwrite("assets", &Agent::assets, "The agent's bundle of assets")
;
py::class_<Good, std::shared_ptr<Good>> good(m, "Good", member);
good.def(py::init<std::string>(), py::arg("name") = "");
bind_firm(m, agent);
bind_market(m, member);
}
}}
#pragma once
#include "pyeris/common.hpp"
#include <pybind11/functional.h>
#include <eris/Member.hpp>
// Basic member base classes support (Member, Agent, Good; Market is in Market.hpp)
//
// We also include a glue class for eris::Member (and related types). This does a few things:
// - it exposes the added()/removed()/weakDepRemoved() methods, which are protected in eris but need
// to be public for pyeris to access.
// - it makes them virtualizable via python methods using pybind11's interface for that
namespace pyeris { namespace core {
class PyMember : public Member {
public:
// Default constructor
PyMember() = default;
// The python interface uses duck typing for period optimizers, which means we need to look for
// them here, and if found, set up dependent optimizer glue instances.
virtual void added() override;
// Expose removed()
virtual void removed() override;
// Expose weakDepRemoved()
virtual void weakDepRemoved(eris::SharedMember<eris::Member> removed, eris_id_t old_id) override;
};
void bind_members(py::module &m);
}}