Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update main with next releasable version #25

Merged
merged 11 commits into from
Jan 3, 2025
Merged
4,615 changes: 1,466 additions & 3,149 deletions pixi.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ requires = [

[project]
name = "upstage-des"
version = "0.2.0"
version = "0.2.1"
description = "A library for behavior-driven discrete event simulation."
readme = "README.md"
keywords = [
Expand Down
2 changes: 1 addition & 1 deletion src/upstage_des/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
"""Declare the UPSTAGE version."""

__authors__ = "UPSTAGE Contributors, GTRI"
__version__ = "0.2.0"
__version__ = "0.2.1"
4 changes: 3 additions & 1 deletion src/upstage_des/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ def __init_states(self, **states: Any) -> None:
exist = set(self._state_defs.keys())
unseen = exist - seen
for state_name in unseen:
if self._state_defs[state_name].has_default():
_state = self._state_defs[state_name]
if _state.has_default():
seen.add(state_name)
_state._set_default(self)
if len(seen) != len(exist):
raise UpstageError(
f"Missing values for states! These states need values: "
Expand Down
24 changes: 24 additions & 0 deletions src/upstage_des/data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,15 @@ def make_location(self) -> CartesianLocation:
x=self.x, y=self.y, z=self.z, use_altitude_units=self.use_altitude_unts
)

def __eq__(self, value: Any) -> bool:
"""Test for equality of two cartesian locations data objects."""
if not isinstance(value, CartesianLocationData):
raise ValueError(
f"Cannot compare a {value.__class__.__name__} to a CartesianLocationData"
)
check = ["x", "y", "z"]
return all(getattr(self, c) == getattr(value, c) for c in check)


class GeodeticLocationData:
"""Object for storing geodetic data without an environment."""
Expand Down Expand Up @@ -596,3 +605,18 @@ def make_location(self) -> GeodeticLocation:
alt=self.alt,
in_radians=self.in_radians,
)

def __eq__(self, value: Any) -> bool:
"""Test for equality of two cartesian locations data objects."""
if not isinstance(value, GeodeticLocationData):
raise ValueError(
f"Cannot compare a {value.__class__.__name__} to a GeodeticLocationData"
)
other = [value.lat, value.lon]
if value.in_radians != self.in_radians:
if self.in_radians:
other = list(map(radians, other))
else:
other = list(map(degrees, other))
angles = [self.lat, self.lon] == other
return angles and self.alt == value.alt
4 changes: 2 additions & 2 deletions src/upstage_des/data_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ def _actor_state_data(

for state_name, state in actor._state_defs.items():
if skip_locations and isinstance(state, LOCATION_TYPES):
break
_value = actor.__dict__[state_name] # skips data recording after the fact
continue
_value = actor.__dict__[state_name]
is_active = isinstance(state, ActiveState)
if state_name in actor._state_histories:
data.extend(
Expand Down
28 changes: 24 additions & 4 deletions src/upstage_des/states.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,23 @@ def __get__(self, instance: "Actor", objtype: type | None = None) -> ST:
value = getattr(actor, name)
self.__set__(instance, value)
if self.name not in instance.__dict__:
# Just set the value to the default
# Mutable types will be tricky here, so deepcopy them
instance.__dict__[self.name] = deepcopy(self._default)
raise SimulationError(f"State {self.name} should have been set.")
v = instance.__dict__[self.name]
return cast(ST, v)

def __set_name__(self, owner: "Actor", name: str) -> None:
self.name = name

def _set_default(self, instance: "Actor") -> None:
"""Set the state's value on the actor the default.

Args:
instance (Actor): Actor holding the state.
"""
assert self._default is not None
value = deepcopy(self._default)
self.__set__(instance, value)

def has_default(self) -> bool:
"""Check if a default exists.

Expand Down Expand Up @@ -283,8 +291,13 @@ def __set__(self, instance: "Actor", value: bool) -> None:
instance (Actor): The actor
value (bool): The value to set
"""
# Setting the default value shouldn't trigger the callback
# to the motion manager.
was_set = True
if self.name not in instance.__dict__:
was_set = False
super().__set__(instance, value)
if hasattr(instance.stage, "motion_manager"):
if hasattr(instance.stage, "motion_manager") and was_set:
mgr = instance.stage.motion_manager
if not value:
mgr._mover_not_detectable(instance)
Expand Down Expand Up @@ -964,6 +977,13 @@ def __set__(self, instance: "Actor", value: dict | Any) -> None:
self._broadcast_change(instance, self.name, value)

def _set_default(self, instance: "Actor") -> None:
"""Set the default conditions.

The empty dictionary input forces default to happen the right way.

Args:
instance (Actor): The actor holding this state.
"""
self.__set__(instance, {})

def __get__(self, instance: "Actor", owner: type | None = None) -> T:
Expand Down
21 changes: 14 additions & 7 deletions src/upstage_des/test/test_data_reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Cashier(UP.Actor):
class Cart(UP.Actor):
location = UP.CartesianLocationChangingState(recording=True)
location_two = UP.CartesianLocationChangingState(recording=True)
holding = UP.State[float](default=0.0, recording=True)


def test_data_reporting() -> None:
Expand Down Expand Up @@ -117,14 +118,19 @@ def test_data_reporting() -> None:
assert ctr[("Ertha", "Cashier", "items_scanned")] == 5
assert ctr[("Ertha", "Cashier", "cue")] == 4
assert ctr[("Ertha", "Cashier", "cue2")] == 4
assert ctr[("Ertha", "Cashier", "time_working")] == 5
assert ctr[("Ertha", "Cashier", "time_working")] == 6
assert ctr[("Bertha", "Cashier", "items_scanned")] == 2
assert ctr[("Bertha", "Cashier", "cue")] == 4
assert ctr[("Bertha", "Cashier", "cue2")] == 4
assert ctr[("Bertha", "Cashier", "time_working")] == 4
assert ctr[("Bertha", "Cashier", "time_working")] == 5
assert ctr[("Store Test", "SelfMonitoringFilterStore", "Resource")] == 3
assert not any(x[0] == "Wobbly Wheel" for x in state_table)
assert len(state_table) == 35
# Test for default values untouched in the sim showing up in the data.
assert ctr[("Wobbly Wheel", "Cart", "holding")] == 1
row = [r for r in state_table if r[:3] == ("Wobbly Wheel", "Cart", "holding")][0]
assert row[4] == 0
assert row[3] == 0.0
# Continuing as before
assert len(state_table) == 38
assert cols == all_cols
assert cols == [
"Entity Name",
Expand All @@ -139,15 +145,16 @@ def test_data_reporting() -> None:
assert ctr[("Ertha", "Cashier", "items_scanned")] == 5
assert ctr[("Ertha", "Cashier", "cue")] == 4
assert ctr[("Ertha", "Cashier", "cue2")] == 4
assert ctr[("Ertha", "Cashier", "time_working")] == 5
assert ctr[("Ertha", "Cashier", "time_working")] == 6
assert ctr[("Bertha", "Cashier", "items_scanned")] == 2
assert ctr[("Bertha", "Cashier", "cue")] == 4
assert ctr[("Bertha", "Cashier", "cue2")] == 4
assert ctr[("Bertha", "Cashier", "time_working")] == 4
assert ctr[("Bertha", "Cashier", "time_working")] == 5
assert ctr[("Store Test", "SelfMonitoringFilterStore", "Resource")] == 3
assert ctr[("Wobbly Wheel", "Cart", "holding")] == 1
assert ctr[("Wobbly Wheel", "Cart", "location")] == 4
assert ctr[("Wobbly Wheel", "Cart", "location_two")] == 4
assert len(all_state_table) == 35 + 8
assert len(all_state_table) == 38 + 8

assert loc_cols == [
"Entity Name",
Expand Down
36 changes: 36 additions & 0 deletions src/upstage_des/test/test_data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,39 @@ def test_geodetic() -> None:

assert loc_up_rad - UP.GeodeticLocation(10, 10) > 0
assert UP.GeodeticLocation(10, 10) - loc_up_rad > 0


def test_data_objects() -> None:
cart1 = UP.CartesianLocationData(1.0, 2.1, 3.2)
cart2 = UP.CartesianLocationData(1.0, 2.1, 3.2)
assert cart1 == cart2

with pytest.raises(ValueError):
cart1 == (1.0, 2.1, 3.2)

geo1 = UP.GeodeticLocationData(13.0, 12.1, 11.2)
geo2 = UP.GeodeticLocationData(13.0, 12.1, 11.2)
assert geo1 == geo2

geo3 = UP.GeodeticLocationData(radians(13.0), radians(12.1), 11.2, in_radians=True)
assert geo1 == geo3
assert geo3 == geo1

with pytest.raises(ValueError):
geo1 == (13.0, 12.1, 11.2)

with UP.EnvironmentContext():
for k, v in STAGE_SETUP.items():
UP.add_stage_variable(k, v)

loc1 = geo1.make_location()
assert isinstance(loc1, UP.GeodeticLocation)

loc3 = geo3.make_location()

assert loc1 - loc3 == 0.0
assert loc1 == loc3

loc1 = cart1.make_location()
loc2 = cart2.make_location()
assert loc1 == loc2
44 changes: 23 additions & 21 deletions src/upstage_des/test/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
class StateTest:
state_one = State[Any]()
state_two = State[Any](recording=True)
lister = State[list](default=[])
diction = State[dict](default={})
setstate = State[set](default=set())

def __init__(self, env: Environment | None) -> None:
cast(UP.Actor, self)
Expand All @@ -44,6 +41,12 @@ class StateTestActor(Actor):
state_three = LinearChangingState(recording=True)


class MutableDefaultActor(Actor):
lister = State[list](default=[])
diction = State[dict](default={})
setstate = State[set](default=set())


def test_state_fails_without_env() -> None:
"""Test that recording states need the class to have an env attribute"""
tester = StateTest(None)
Expand Down Expand Up @@ -77,23 +80,23 @@ def test_state_recording() -> None:


def test_state_mutable_default() -> None:
with EnvironmentContext(initial_time=1.5) as env:
tester = StateTest(env)
tester2 = StateTest(env)
assert id(tester.lister) != id(tester2.lister) # type: ignore [arg-type]
tester.lister.append(1) # type: ignore [arg-type]
assert len(tester2.lister) == 0 # type: ignore [arg-type]
assert len(tester.lister) == 1 # type: ignore [arg-type]
with EnvironmentContext(initial_time=1.5):
tester = MutableDefaultActor(name="Example")
tester2 = MutableDefaultActor(name="Example2")
assert id(tester.lister) != id(tester2.lister)
tester.lister.append(1)
assert len(tester2.lister) == 0
assert len(tester.lister) == 1

assert id(tester.diction) != id(tester2.diction) # type: ignore [arg-type]
tester2.diction[1] = 2 # type: ignore [arg-type]
assert len(tester.diction) == 0 # type: ignore [arg-type]
assert len(tester2.diction) == 1 # type: ignore [arg-type]
assert id(tester.diction) != id(tester2.diction)
tester2.diction[1] = 2
assert len(tester.diction) == 0
assert len(tester2.diction) == 1

assert id(tester.setstate) != id(tester2.setstate) # type: ignore [arg-type]
tester2.setstate.add(1) # type: ignore [arg-type]
assert len(tester.setstate) == 0 # type: ignore [arg-type]
assert len(tester2.setstate) == 1 # type: ignore [arg-type]
assert id(tester.setstate) != id(tester2.setstate)
tester2.setstate.add(1)
assert len(tester.setstate) == 0
assert len(tester2.setstate) == 1


def test_state_values_from_init() -> None:
Expand Down Expand Up @@ -232,9 +235,8 @@ class HolderBad(Actor):
assert h.res2.capacity == 12
assert h.res2.level == 5

hb = HolderBad(name="Bad one")
with pytest.raises(UpstageError):
hb.res2.capacity
HolderBad(name="Bad one")


def test_resource_state_kind_init() -> None:
Expand Down Expand Up @@ -339,7 +341,7 @@ def test_matching_states() -> None:
"""

class Worker(UP.Actor):
sleepiness = UP.State(default=0, valid_types=(float,))
sleepiness = UP.State[float](default=0.0, valid_types=(float,))
walkie = UP.CommunicationStore(mode="UHF")
intercom = UP.CommunicationStore(mode="loudspeaker")

Expand Down
Loading