Coverage for C:\Git\TUDelft-CITG\Hydraulic-Infrastructure-Realisation\digital_twin\core.py : 39%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
# -*- coding: utf-8 -*-
# package(s) related to time, space and id
# you need these dependencies (you can get these from anaconda) # package(s) related to the simulation
# spatial libraries
# additional packages
"""General object which can be extended by any class requiring a simpy environment
env: a simpy Environment """
"""Something that has a name and id
name: a name id: a unique id generated with uuid"""
"""Initialization""" # generate some id, in this case based on m
"""Something with a geometry (geojson format)
geometry: can be a point as well as a polygon"""
"""Initialization"""
"""Container class
capacity: amount the container can hold level: amount the container holds initially container: a simpy object that can hold stuff"""
"""Initialization"""
"""EnergyUse class
energy_use_sailing: function that specifies the fuel use during sailing activity - input should be time energy_use_loading: function that specifies the fuel use during loading activity - input should be time energy_use_unloading: function that specifies the fuel use during unloading activity - input should be time
At the moment "keeping track of fuel" is not added to the digital twin.
Example function could be as follows. The energy use of the loading event is equal to: duration * power_use.
def energy_use_loading(power_use): return lambda x: x * power_use """
"""Initialization"""
"""Using values from Becker [2014], https://www.sciencedirect.com/science/article/pii/S0301479714005143.
The values are slightly modified, there is no differences in dragead / bucket drip / cutterhead within this class sigma_d = source term fraction due to dredging sigma_o = source term fraction due to overflow sigma_p = source term fraction due to placement f_sett = fraction of fines that settle within the hopper f_trap = fraction of fines that are trapped within the hopper """
super().__init__(*args, **kwargs) """Initialization"""
self.sigma_d = sigma_d self.sigma_o = sigma_o self.sigma_p = sigma_p self.f_sett = f_sett self.f_trap = f_trap
self.m_r = 0
"""Condition to stop dredging if certain spill limits are exceeded
limit = limit of kilograms spilled material start = start of the condition end = end of the condition """
super().__init__(*args, **kwargs) """Initialization""" limits = [] starts = [] ends = []
if type(conditions) == list: for condition in conditions: limits.append(simpy.Container(self.env, capacity = condition.spill_limit)) starts.append(time.mktime(condition.start.timetuple())) ends.append(time.mktime(condition.end.timetuple()))
else: limits.append(simpy.Container(self.env, capacity = conditions.spill_limit)) starts.append(time.mktime(conditions.start.timetuple())) ends.append(time.mktime(conditions.end.timetuple()))
self.SpillConditions = pd.DataFrame.from_dict({"Spill limit": limits, "Criterion start": starts, "Criterion end": ends})
tolerance = math.inf waiting = 0
for i in self.SpillConditions.index:
if self.SpillConditions["Criterion start"][i] <= self.env.now and self.env.now <= self.SpillConditions["Criterion end"][i]: tolerance = self.SpillConditions["Spill limit"][i].capacity - self.SpillConditions["Spill limit"][i].level
if tolerance < spill: waiting = self.SpillConditions["Criterion end"][i]
while i + 1 != len(self.SpillConditions.index) and tolerance < spill: if self.SpillConditions["Criterion end"][i] == self.SpillConditions["Criterion start"][i + 1]: tolerance = self.SpillConditions["Spill limit"][i + 1].capacity - self.SpillConditions["Spill limit"][i + 1].level waiting = self.SpillConditions["Criterion end"][i + 1]
i += 1
return waiting
"""Condition to stop dredging if certain spill limits are exceeded
limit = limit of kilograms spilled material start = start of the condition end = end of the condition """
super().__init__(*args, **kwargs) """Initialization""" self.spill_limit = spill_limit self.start = start self.end = end
"""Using relations from Becker [2014], https://www.sciencedirect.com/science/article/pii/S0301479714005143."""
super().__init__(*args, **kwargs) """Initialization"""
"""Calculate the spill due to the dredging activity
density = the density of the dredged material fines = the percentage of fines in the dredged material volume = the dredged volume dredging_duration = duration of the dredging event overflow_duration = duration of the dredging event whilst overflowing
m_t = total mass of dredged fines per cycle m_d = total mass of spilled fines during one dredging event m_h = total mass of dredged fines that enter the hopper
m_o = total mass of fine material that leaves the hopper during overflow m_op = total mass of fines that are released during overflow that end in dredging plume m_r = total mass of fines that remain within the hopper"""
m_t = density * fines * volume m_d = processor.sigma_d * m_t m_h = m_t - m_d
m_o = (overflow_duration / dredging_duration) * (1 - mover.f_sett) * (1 - mover.f_trap) * m_h m_op = mover.sigma_o * m_o mover.m_r = m_h - m_o
if isinstance(self, Log): self.log_entry("fines released", self.env.now, m_d + m_op, self.geometry)
return m_d + m_op
"""Calculate the spill due to the placement activity""" if isinstance(self, Log): self.log_entry("fines released", self.env.now, mover.m_r * processor.sigma_p, self.geometry)
return mover.m_r * processor.sigma_p
""" Create a soillayer
layer = layer number, 0 to n, with 0 the layer at the surface material = name of the dredged material density = density of the dredged material fines = fraction of total that is fine material """
super().__init__(*args, **kwargs) """Initialization""" self.layer = layer self.volume = volume self.material = material self.density = density self.fines = fines
""" Add soil properties to an object
soil = list of SoilLayer objects """
super().__init__(*args, **kwargs) """Initialization"""
self.soil = {}
"""Add a layer based on a SoilLayer object.""" for key in self.soil: if key == "Layer {:04d}".format(soillayer.layer): print("Soil layer named **Layer {:04d}** already exists".format(soillayer.layer))
# Add soillayer to self self.soil["Layer {:04d}".format(soillayer.layer)] = {"Layer": soillayer.layer, "Volume": soillayer.volume, "Material": soillayer.material, "Density": soillayer.density, "Fines": soillayer.fines}
# Make sure that self.soil is always a sorted dict based on layernumber soil = copy.deepcopy(self.soil) self.soil = {}
for key in sorted(soil): self.soil[key] = soil[key]
"""Add a list layers based on a SoilLayer object.""" for layer in soillayers: self.add_layer(layer)
"""Determine the total volume of soil.""" total_volume = 0
for layer in self.soil: total_volume += self.soil[layer]["Volume"]
return total_volume
"""Create a new SoilLayer object based on the weighted average parameters of extracted layers.
len(layers) should be len(volumes)""" densities = [] fines = [] name = "Mixture of: "
for i, layer in enumerate(layers): if 0 < volumes[i]: densities.append(self.soil[layer]["Density"]) fines.append(self.soil[layer]["Fines"]) name += (self.soil[layer]["Material"] + ", ") else: densities.append(0) fines.append(0)
return SoilLayer(0, sum(volumes), name.rstrip(", "), np.average(np.asarray(densities), weights = np.asarray(volumes)), np.average(np.asarray(fines), weights = np.asarray(volumes)))
"""Remove soil from self."""
# If soil is a mover, the mover should be initialized with an empty soil dict after emptying if isinstance(self, Movable) and volume == self.container.level: removed_soil = list(self.soil.items())[0]
self.soil = {}
return SoilLayer(0, removed_soil[1]["Volume"], removed_soil[1]["Material"], removed_soil[1]["Density"], removed_soil[1]["Fines"])
# In all other cases the soil dict should remain, with updated values else: removed_volume = 0 layers = [] volumes = []
for layer in sorted(self.soil): if (volume - removed_volume) <= self.soil[layer]["Volume"]: layers.append(layer) volumes.append(volume - removed_volume)
self.soil[layer]["Volume"] -= (volume - removed_volume)
break
else: removed_volume += self.soil[layer]["Volume"] layers.append(layer) volumes.append(self.soil[layer]["Volume"])
self.soil[layer]["Volume"] = 0
return self.weighted_average(layers, volumes)
"""Add soil to self.
Add a layer based on a SoilLayer object.""" # If already soil available if self.soil: # Can be moveable --> mix if isinstance(self, Movable): pass
# Can be site --> add layer or add volume else: top_layer = list(sorted(self.soil.keys()))[0]
# If toplayer material is similar to added material --> add volume if (self.soil[top_layer]["Material"] == soillayer.material and \ self.soil[top_layer]["Density"] == soillayer.density and \ self.soil[top_layer]["Fines"] == soillayer.fines):
self.soil[top_layer]["Volume"] += soillayer.volume
# If not --> add layer else: layers = copy.deepcopy(self.soil) self.soil = {} self.add_layer(soillayer)
for key in sorted(layers): layers[key]["Layer"] += 1 self.add_layer(SoilLayer(layers[key]["Layer"], layers[key]["Volume"], layers[key]["Material"], layers[key]["Density"], layers[key]["Fines"]))
# If no soil yet available, add layer else: self.add_layer(soillayer)
"""Get the soil properties for a certain amount""" volumes = [] layers = [] volume = 0
for layer in sorted(self.soil): if (amount - volume) <= self.soil[layer]["Volume"]: volumes.append(amount - volume) layers.append(layer) break else: volumes.append(self.soil[layer]["Volume"]) layers.append(layer) volume += self.soil[layer]["Volume"]
properties = self.weighted_average(layers, volumes)
return properties.density, properties.fines
"""HasWeather class
Used to add weather conditions to a project site name: name of .csv file in folder
year: name of the year column month: name of the month column day: name of the day column
timestep: size of timestep to interpolate between datapoints (minutes) bed: level of the seabed / riverbed with respect to CD (meters) """
super().__init__(*args, **kwargs) """Initialization""" df = pd.read_csv(file) df.index = df[[year, month, day, hour]].apply(lambda s : datetime.datetime(*s), axis = 1) df = df.drop([year, month, day, hour],axis=1)
self.timestep = datetime.timedelta(minutes = timestep)
data = {} for key in df: series = (pd.Series(df[key], index = df.index) .fillna(0) .resample(self.timestep) .interpolate("linear"))
data[key] = series.values
data["Index"] = series.index self.metocean_data = pd.DataFrame.from_dict(data) self.metocean_data.index = self.metocean_data["Index"] self.metocean_data.drop(["Index"], axis = 1, inplace = True)
if bed: self.metocean_data["Water depth"] = self.metocean_data["Tide"] - bed
"""HasWorkabilityCriteria class
Used to add workability criteria """
super().__init__(*args, **kwargs) """Initialization""" self.v = v self.wgs84 = pyproj.Geod(ellps='WGS84')
"""WorkabilityCriterion class
Used to add limits to vessels (and therefore acitivities) condition: column name of the metocean data (Hs, Tp, etc.) maximum: maximum value minimum: minimum value window_length: minimal length of the window (minutes)"""
super().__init__(*args, **kwargs) """Initialization""" self.wgs84 = pyproj.Geod(ellps='WGS84')
"""HasDepthRestriction class
Used to add depth limits to vessels draught: should be a lambda function with input variable container.volume waves: list with wave_heights ukc: list with ukc, corresponding to wave_heights """
super().__init__(*args, **kwargs) """Initialization""" self.compute_draught = compute_draught self.waves = waves self.ukc = ukc self.filling = filling
self.depth_data = {}
fill_degree = self.container.level / self.container.capacity time = datetime.datetime.utcfromtimestamp(self.env.now) waiting = 0
for key in sorted(self.depth_data[location.name].keys()): if fill_degree <= key: series = self.depth_data[location.name][key]["Series"]
if len(series) == 0: print("No actual allowable draught available - starting anyway.") waiting = 0
else: a = series.values v = np.datetime64(time - location.timestep)
index = np.searchsorted(a, v, side='right')
try: next_window = series[index] - time except IndexError: print("Length weather data exceeded - continuing without weather.") next_window = series[-1] - time
waiting = max(next_window, datetime.timedelta(0)).total_seconds()
break
if waiting != 0: self.log_entry('waiting for tide start', self.env.now, waiting, self.geometry) yield self.env.timeout(waiting) self.log_entry('waiting for tide stop', self.env.now, waiting, self.geometry)
# Minimal waterdepth should be draught + ukc # Waterdepth is tide - depth site # For full to empty [0%, 20%, 40%, 60%, 80%, 100%]
self.depth_data[location.name] = {}
for i in np.linspace(0.20, 1, 9): df = location.metocean_data.copy()
draught = self.compute_draught(i) df["Required depth"] = df["Hs"].apply(lambda s : self.calc_required_depth(draught, s)) series = pd.Series(df["Required depth"] < df["Water depth"])
# Make a series on which the activity can start duration = self.unloading_func(i * self.container.capacity) steps = max(int(duration / location.timestep.seconds + .5), 1) windowed = series.rolling(steps) windowed = windowed.max().shift(-steps + 1) windowed = windowed[windowed.values == 1].index
self.depth_data[location.name][i] = {"Volume": i * self.container.capacity, "Draught": draught, "Series": windowed}
required_depth = np.nan
for i, wave in enumerate(self.waves): if wave_height <= wave: required_depth = self.ukc[i] + draught
return required_depth
# Calculate depth restrictions if not self.depth_data: if isinstance(origin, HasWeather): self.calc_depth_restrictions(origin) if isinstance(destination, HasWeather): self.calc_depth_restrictions(destination)
# If a filling degee has been specified if self.filling: return self.filling * self.container.capacity
# If not, try to optimize the load with regard to the tidal window else: loads = [] waits = []
amounts = [] time = datetime.datetime.utcfromtimestamp(self.env.now) fill_degrees = self.depth_data[destination.name].keys()
for filling in fill_degrees: series = self.depth_data[destination.name][filling]["Series"]
if len(series) != 0: # Determine length of cycle loading = loader.loading_func(filling * self.container.capacity)
orig = shapely.geometry.asShape(origin.geometry) dest = shapely.geometry.asShape(destination.geometry) _, _, distance = self.wgs84.inv(orig.x, orig.y, dest.x, dest.y) sailing_full = distance / self.compute_v(0) sailing_full = distance / self.compute_v(filling)
duration = datetime.timedelta(seconds = (sailing_full + loading + sailing_full))
# Determine waiting time a = series.values v = np.datetime64(time + duration)
index = np.searchsorted(a, v, side='right') next_window = series[index] - duration - time
waiting = max(next_window, datetime.timedelta(0)).total_seconds()
# In case waiting is always required loads.append(filling * self.container.capacity) waits.append(waiting)
if waiting < destination.timestep.total_seconds(): amounts.append(filling * self.container.capacity)
# Check if there is a better filling degree if amounts: return max(amounts) elif loads: cargo = 0
for i, _ in enumerate(loads): if waits[i] == min(waits): cargo = loads[i]
return cargo
def current_draught(self): return self.compute_draught(self.container.level / self.container.capacity)
"""Movable class
Used for object that can move with a fixed speed geometry: point used to track its current location v: speed"""
"""Initialization"""
"""determine distance between origin and destination, and yield the time it takes to travel it""" # Determine distance based on geometry objects
# Determine speed based on filling degree
# Check out the time based on duration of sailing event
# Set mover geometry to destination geometry
# Compute the energy use
# Debug logs
current_location = shapely.geometry.asShape(self.geometry) other_location = shapely.geometry.asShape(locatable.geometry) _, _, distance = self.wgs84.inv(current_location.x, current_location.y, other_location.x, other_location.y)
return distance < tolerance
# message depends on filling degree: if container is empty --> sailing empt message = "Energy use sailing empty" else:
def current_speed(self):
"""ContainerDependentMovable class
Used for objects that move with a speed dependent on the container level compute_v: a function, given the fraction the container is filled (in [0,1]), returns the current speed"""
compute_v, *args, **kwargs): """Initialization"""
def current_speed(self):
"""HasProcessingLimit class
Adds a limited Simpy resource which should be requested before the object is used for processing."""
"""Initialization"""
"""Log class
log: log message [format: 'start activity' or 'stop activity'] t: timestamp value: a value can be logged as well geometry: value from locatable (lat, lon)"""
"""Initialization""" "Timestamp": [], "Value": [], "Geometry": []}
"""Log"""
json = [] for msg, t, value, geometry_log in zip(self.log["Message"], self.log["Timestamp"], self.log["Value"], self.log["Geometry"]): json.append(dict(message=msg, time=t, value=value, geometry_log=geometry_log)) return json
"""Processor class
loading_func: lambda function to determine the duration of loading event based on input parameter amount unloading_func: lambda function to determine the duration of unloading event based on input parameter amount
Example function could be as follows. The duration of the loading event is equal to: amount / rate.
def loading_func(loading_rate): return lambda x: x / loading_rate
A more complex example function could be as follows. The duration of the loading event is equal to: manoeuvring + amount / rate + cleaning.
def loading_func(manoeuvring, loading_rate, cleaning): return lambda x: datetime.timedelta(minutes = manoeuvring).total_seconds() + \ x / loading_rate + \ datetime.timedelta(minutes = cleaning).total_seconds()
"""
"""Initialization"""
# noinspection PyUnresolvedReferences """get amount from origin container, put amount in destination container, and yield the time it takes to process it"""
# Before starting to process, check the following requirements # Make sure that the origin and destination have storage # Make sure that the origin and destination allow processing # Make sure that the processor, origin and destination can log the events # Make sure that the processor, origin and destination are all at the same location # Make sure that the volume of the origin is equal, or smaller, than the requested amount # Make sure that the container of the destination is sufficiently large
# Make sure all requests are granted # Request access to the origin # Request access to the destination # Yield the requests once granted
# If requests are yielded, start activity
# Activity can only start if environmental conditions allow it # Waiting event should be combined to check if all conditions allow starting
# Check weather # yield from self.checkWeather()
# Check tide
# Check spill
# Add spill the location where processing is taking place
# Shift soil from container volumes
# Shift volumes in containers
# Checkout the time
# Compute the energy use
# Log the end of the activity
""" duration: duration of the activity in seconds origin: origin of the moved volume (the computed amount) destination: destination of the moved volume (the computed amount)
There are three options: 1. Processor is also origin, destination could consume energy 2. Processor is also destination, origin could consume energy 3. Processor is neither destination, nor origin, but both could consume energy """
# If self == origin --> unloading energy = destination.energy_use_loading(duration) message = "Energy use loading" destination.log_entry(message, self.env.now, energy, destination.geometry)
# If self == destination --> loading energy = origin.energy_use_unloading(duration) message = "Energy use unloading" origin.log_entry(message, self.env.now, energy, origin.geometry)
# If self != origin and self != destination --> processing else:
""" duration: duration of the activity in seconds origin: origin of the moved volume (the computed amount) destination: destination of the moved volume (the computed amount)
There are three options: 1. Processor is also origin, destination could have spill requirements 2. Processor is also destination, origin could have spill requirements 3. Processor is neither destination, nor origin, but both could have spill requirements
Result of this function is possible waiting, spill is added later on and does not depend on possible requirements """
# If self == origin --> destination is a placement location density, fines = self.get_properties(amount) spill = self.sigma_d * density * fines * amount
waiting = destination.check_conditions(spill)
if 0 < waiting: self.log_entry('waiting for spill start', self.env.now, 0, self.geometry) yield self.env.timeout(waiting - self.env.now) self.log_entry('waiting for spill stop', self.env.now, 0, self.geometry)
# If self == destination --> origin is a retrieval location density, fines = origin.get_properties(amount) spill = self.sigma_d * density * fines * amount
waiting = origin.check_conditions(spill)
if 0 < waiting: self.log_entry('waiting for spill start', self.env.now, 0, self.geometry) yield self.env.timeout(waiting - self.env.now) self.log_entry('waiting for spill stop', self.env.now, 0, self.geometry)
# If self != origin and self != destination --> processing else: density, fines = origin.get_properties(amount) spill = self.sigma_d * density * fines * amount
waiting = destination.check_conditions(spill)
if 0 < waiting: self.log_entry('waiting for spill start', self.env.now, 0, self.geometry) yield self.env.timeout(waiting - self.env.now) self.log_entry('waiting for spill stop', self.env.now, 0, self.geometry)
density, fines = origin.get_properties(amount) spill = self.sigma_d * density * fines * amount
waiting = origin.check_conditions(spill)
if 0 < waiting: self.log_entry('waiting for spill start', self.env.now, 0, self.geometry) yield self.env.timeout(waiting - self.env.now) self.log_entry('waiting for spill stop', self.env.now, 0, self.geometry)
yield from origin.check_depth_restriction(destination) yield from destination.check_depth_restriction(origin)
""" duration: duration of the activity in seconds origin: origin of the moved volume (the computed amount) destination: destination of the moved volume (the computed amount)
There are three options: 1. Processor is also origin, destination could have spill requirements 2. Processor is also destination, origin could have spill requirements 3. Processor is neither destination, nor origin, but both could have spill requirements
Result of this function is possible waiting, spill is added later on and does not depend on possible requirements """
density, fines = origin.get_properties(amount)
# If self == origin --> destination is a placement location if self == origin: if isinstance(self, HasPlume) and isinstance(destination, HasSpill): spill = destination.spillPlacement(self, self)
if 0 < spill and isinstance(destination, HasSpillCondition): for condition in destination.SpillConditions["Spill limit"]: condition.put(spill)
# If self == destination --> origin is a retrieval location elif self == destination: if isinstance(self, HasPlume) and isinstance(origin, HasSpill): spill = origin.spillDredging(self, self, density, fines, amount, duration)
if 0 < spill and isinstance(origin, HasSpillCondition): for condition in origin.SpillConditions["Spill limit"]: condition.put(spill)
# If self != origin and self != destination --> processing else: if isinstance(self, HasPlume) and isinstance(destination, HasSpill): spill = destination.spillPlacement(self, self)
if 0 < spill and isinstance(destination, HasSpillCondition): for condition in destination.SpillConditions["Spill limit"]: condition.put(spill)
if isinstance(self, HasPlume) and isinstance(origin, HasSpill): spill = origin.spillDredging(self, self, density, fines, amount, duration)
if 0 < spill and isinstance(origin, HasSpillCondition): for condition in origin.SpillConditions["Spill limit"]: condition.put(spill)
""" origin: origin of the moved volume (the computed amount) destination: destination of the moved volume (the computed amount) amount: the volume of soil that is moved
Can only occur if both the origin and the destination have soil objects (mix-ins) """
soil = origin.get_soil(amount) destination.put_soil(soil)
soil = origin.get_soil(amount)
soil = SoilLayer(0, amount, "Unknown", 0, 0) destination.put_soil(soil)
"""serialize a simpy digital_twin object to json""" result = {} for key, val in o.__dict__.items(): if isinstance(val, simpy.Environment): continue if isinstance(val, simpy.Container): result['capacity'] = val.capacity result['level'] = val.level elif isinstance(val, simpy.Resource): result['nr_resources'] = val.capacity else: result[key] = val
return result
return json.dumps(obj, cls=DictEncoder) |