Skip to content

Commit

Permalink
Landscape Configuration (#79)
Browse files Browse the repository at this point in the history
Added LandscapeConfiguration - structure that could response on request about environment (the environment remains static over time), and LandGraph, that describes a graph of environment:

vertices - platforms, resource holders
edges - roads with own weight (length) and bandwidth
Developed algorithm of unrenewable resource delivery before the start of a work.
SupplyTimeline was developed to take into account the resources' of:

holders: materials (there is no way to make up for them), vehicles (renewable resources, because they could leave their holder and come back after delivery),
roads: vehicles (this is represents of road bandwidth, it is renewable resource)
PlatformTimeline is presented to avoid implementation difficult platforms' behaviour. The state of platform, that distributed over the time, has follow rules:

timestamp has resources that are left AFTER the work, that started at the time, obtain necessary materials,
the work can be scheduled at the timestamp A if ALL timestamps after 'A' can provide necessary materials (not other way).
Moreover, SimpleSynthetic was extended by LandscapeConfiguration generator based on obtained WorkGraph.

In addition, tests include flag about MaterialReq generation for each GraphNode in WorkGraph.

PlatformTimeline is the beginning of separation SupplyTimeline into individual timelines to consider the complex behaviour of each participant in evironment (holders, roads, vehicles, platforms).

Added examples that demonstrate the work with landscape generator and without it.
  • Loading branch information
vanoha authored Apr 27, 2024
1 parent 6f232b8 commit dc365c7
Show file tree
Hide file tree
Showing 34 changed files with 2,369 additions and 395 deletions.
45 changes: 45 additions & 0 deletions examples/landscape_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from sampo.generator import SimpleSynthetic
from sampo.generator.environment import get_contractor_by_wg
from sampo.pipeline.default import DefaultInputPipeline
from sampo.scheduler import GeneticScheduler
from sampo.utilities.visualization import VisualizationMode

if __name__ == '__main__':

# Set up scheduling algorithm and project's start date
start_date = "2023-01-01"

# Set up visualization mode (ShowFig or SaveFig) and the gant chart file's name (if SaveFig mode is chosen)
visualization_mode = VisualizationMode.ShowFig
gant_chart_filename = './output/synth_schedule_gant_chart.png'

# Generate synthetic graph with material requirements for
# number of unique works names and number of unique resources
ss = SimpleSynthetic(rand=31)
wg = ss.small_work_graph()
wg = ss.set_materials_for_wg(wg)
landscape = ss.synthetic_landscape(wg)

# Be careful with the high number of generations and size of population
# It can lead to a long time of the scheduling process because of landscape complexity
scheduler = GeneticScheduler(number_of_generation=1,
mutate_order=0.05,
mutate_resources=0.005,
size_of_population=10)

# Get information about created LandscapeConfiguration
platform_number = len(landscape.platforms)
is_all_nodes_have_materials = all([node.work_unit.need_materials() for node in wg.nodes])
print(f'LandscapeConfiguration: {platform_number} platforms, '
f'All nodes have materials: {is_all_nodes_have_materials}')

# Get list with the Contractor object, which can satisfy the created WorkGraph's resources requirements
contractors = [get_contractor_by_wg(wg)]

project = DefaultInputPipeline() \
.wg(wg) \
.contractors(contractors) \
.landscape(landscape) \
.schedule(scheduler) \
.visualization('2023-01-01')[0] \
.show_gant_chart()
77 changes: 77 additions & 0 deletions experiments/genetic_landscape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import random

import pandas as pd

from sampo.generator import SimpleSynthetic
from sampo.generator.environment import get_contractor_by_wg
from sampo.pipeline import DefaultInputPipeline
from sampo.scheduler import GeneticScheduler
from sampo.schemas.time_estimator import DefaultWorkEstimator

work_time_estimator = DefaultWorkEstimator()


def run_test(args):
graph_size, iterations = args
# global seed

result = []
for i in range(iterations):
rand = random.Random()
ss = SimpleSynthetic(rand=rand)
if graph_size < 100:
wg = ss.small_work_graph()
else:
wg = ss.work_graph(top_border=graph_size)

wg = ss.set_materials_for_wg(wg)
contractors = [get_contractor_by_wg(wg, contractor_id=str(i), contractor_name='Contractor' + ' ' + str(i + 1))
for i in range(1)]

landscape = ss.synthetic_landscape(wg)
scheduler = GeneticScheduler(number_of_generation=1,
mutate_order=0.05,
mutate_resources=0.005,
size_of_population=1,
work_estimator=work_time_estimator,
rand=rand)
schedule = DefaultInputPipeline() \
.wg(wg) \
.contractors(contractors) \
.work_estimator(work_time_estimator) \
.landscape(landscape) \
.schedule(scheduler) \
.finish()
result.append(schedule[0].schedule.execution_time)

# seed += 1

return result


# Number of iterations for each graph size
total_iters = 1
# Number of graph sizes
graphs = 1
# Graph sizes
sizes = [100 * i for i in range(1, graphs + 1)]
total_results = []
# Seed for random number generator can be specified here
# seed = 1

# Iterate over graph sizes and receive results
for size in sizes:
results_by_size = run_test((size, total_iters))
total_results.append(results_by_size)
print(size)

# Save results to the DataFrame
result_df = {'size': [], 'makespan': []}
for i, results_by_size in enumerate(total_results):
result = results_by_size[0]

result_df['size'].append(sizes[i])
result_df['makespan'].append(result)

pd.DataFrame(result_df).to_csv('landscape_genetic_results.csv', index=False)

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
[tool.poetry]
name = "sampo"
version = "0.1.1.304"
version = "0.1.1.341"
description = "Open-source framework for adaptive manufacturing processes scheduling"
authors = ["iAirLab <[email protected]>"]
license = "BSD-3-Clause"
# readme = "README.rst"
# readme = "README.md"
# build = "build.py"
#log_cli = 1

[tool.poetry.dependencies]
python = ">=3.10,<3.11"
Expand Down
4 changes: 1 addition & 3 deletions sampo/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import logging

import sampo.scheduler

from sampo.backend.default import DefaultComputationalBackend

logging.basicConfig(format='[%(name)s] [%(levelname)s] %(message)s', level=logging.NOTSET)
logging.basicConfig(format='[%(name)s] [%(levelname)s] %(message)s', level=logging.INFO)


class SAMPO:
Expand Down
39 changes: 39 additions & 0 deletions sampo/generator/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from sampo.generator import SyntheticGraphType
from sampo.generator.environment import get_contractor
from sampo.generator.environment.landscape import get_landscape_by_wg
from sampo.generator.pipeline.extension import extend_names, extend_resources
from sampo.generator.pipeline.project import get_small_graph, get_graph
from sampo.schemas import LandscapeConfiguration, MaterialReq
from sampo.schemas.graph import WorkGraph


Expand Down Expand Up @@ -71,3 +73,40 @@ def advanced_work_graph(self, works_count_top_border: int, uniq_works: int, uniq
wg = extend_names(uniq_works, wg, self._rand)
wg = extend_resources(uniq_resources, wg, self._rand)
return wg

def set_materials_for_wg(self, wg: WorkGraph, materials_name: list[str] = None, bottom_border: int = None,
top_border: int = None) -> WorkGraph:
"""
Sets the materials for nodes of given work graph
:param top_border: the top border for the number of material kinds in each node (except service nodes)
:param bottom_border: the bottom border for the number of material kinds in each node (except service nodes)
:param materials_name: a list of material names, that can be sent
:return: work graph with materials
"""
if materials_name is None:
materials_name = ['stone', 'brick', 'sand', 'rubble', 'concrete', 'metal']
bottom_border = 2
top_border = 6
else:
if bottom_border is None:
bottom_border = len(materials_name) // 2
if top_border is None:
top_border = len(materials_name)

if bottom_border > len(materials_name) or top_border > len(materials_name):
raise ValueError('The borders are out of the range of materials_name')

for node in wg.nodes:
if not node.work_unit.is_service_unit:
work_materials = list(set(self._rand.choices(materials_name, k=self._rand.randint(bottom_border, top_border))))
node.work_unit.material_reqs = [MaterialReq(name, self._rand.randint(52, 345), name) for name in
work_materials]

return wg

def synthetic_landscape(self, wg: WorkGraph) -> LandscapeConfiguration:
"""
Generates a landscape by work graph
:return: LandscapeConfiguration
"""
return get_landscape_by_wg(wg, self._rand)
175 changes: 175 additions & 0 deletions sampo/generator/environment/landscape.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import math
import random
import uuid
from collections import defaultdict

from sampo.schemas import Material, WorkGraph
from sampo.schemas.landscape import ResourceHolder, Vehicle, LandscapeConfiguration
from sampo.schemas.landscape_graph import LandGraphNode, ResourceStorageUnit, LandGraph


def setup_landscape(platforms_info: dict[str, dict[str, int]],
warehouses_info: dict[str, list[dict[str, int], list[tuple[str, dict[str, int]]]]],
roads_info: dict[str, list[tuple[str, float, int]]]) -> LandscapeConfiguration:
"""
Build landscape configuration based on the provided information with structure as below.
Attributes:
Platform_info structure:
{platform_name:
{material_name: material_count}
}
Warehouse_info structure:
{holder_name:
[
{material_name: material_count},
[(vehicle_name, {vehicle_material_name: vehicle_material_count})]
]
}
Roads_info structure:
{platform_name:
[(neighbour_name, road_length, road_workload)]
}
:return: landscape configuration
"""
name2platform: dict[str, LandGraphNode] = {}
holders: list[ResourceHolder] = []
for platform, platform_info in platforms_info.items():
node = LandGraphNode(str(uuid.uuid4()), platform, ResourceStorageUnit(
{name: count for name, count in platform_info.items()}
))
name2platform[platform] = node

for holder_name, holder_info in warehouses_info.items():
materials = holder_info[0]
vehicles = holder_info[1]
holder_node = LandGraphNode(
str(uuid.uuid4()), holder_name, ResourceStorageUnit(
{name: count for name, count in materials.items()}
))
name2platform[holder_name] = holder_node
holders.append(ResourceHolder(
str(uuid.uuid4()), holder_name,
[
Vehicle(str(uuid.uuid4()), name, [
Material(str(uuid.uuid4()), mat_name, mat_count)
for mat_name, mat_count in vehicle_mat_info.items()
])
for name, vehicle_mat_info in vehicles
],
holder_node
))

for from_node, adj_list in roads_info.items():
name2platform[from_node].add_neighbours([(name2platform[node], length, workload)
for node, length, workload in adj_list])

platforms: list[LandGraphNode] = list(name2platform.values())

return LandscapeConfiguration(
holders=holders,
lg=LandGraph(nodes=platforms)
)


def get_landscape_by_wg(wg: WorkGraph, rnd: random.Random) -> LandscapeConfiguration:
nodes = wg.nodes
max_materials = defaultdict(int)

for node in nodes:
for mat in node.work_unit.need_materials():
if mat.name not in max_materials:
max_materials[mat.name] = mat.count
else:
max_materials[mat.name] = max(max_materials[mat.name], mat.count)

platforms_number = math.ceil(math.log(wg.vertex_count))
platforms = []
materials_name = list(max_materials.keys())

for i in range(platforms_number):
platforms.append(LandGraphNode(str(uuid.uuid4()), f'platform{i}',
ResourceStorageUnit(
{
# name: rnd.randint(max(max_materials[name], 1),
# 2 * max(max_materials[name], 1))
name: max(max_materials[name], 1)
for name in materials_name
}
)))

for i, platform in enumerate(platforms):
if i == platforms_number - 1:
continue
neighbour_platforms = rnd.choices(platforms[i + 1:], k=rnd.randint(1, math.ceil(len(platforms[i + 1:]) / 3)))

neighbour_platforms_tmp = neighbour_platforms.copy()
for neighbour in neighbour_platforms:
if neighbour in platform.neighbours:
neighbour_platforms_tmp.remove(neighbour)
neighbour_platforms = neighbour_platforms_tmp

# neighbour_edges = [(neighbour, rnd.uniform(1.0, 10.0), rnd.randint(wg.vertex_count, wg.vertex_count * 2))
# for neighbour in neighbour_platforms]
lengths = [i * 50 for i in range(1, len(neighbour_platforms) + 1)]
neighbour_edges = [(neighbour, lengths[i], wg.vertex_count)
for i, neighbour in enumerate(neighbour_platforms)]
platform.add_neighbours(neighbour_edges)

inseparable_heads = [node for node in nodes if not node.is_inseparable_son()]

platforms_tmp = ((len(inseparable_heads) // platforms_number) * platforms +
platforms[:len(inseparable_heads) % platforms_number])
rnd.shuffle(platforms_tmp)

for node, platform in zip(inseparable_heads, platforms_tmp):
if not node.work_unit.is_service_unit:
for ins_child in node.get_inseparable_chain_with_self():
platform.add_works(ins_child)

holders_number = math.ceil(math.sqrt(math.log(wg.vertex_count)))
holders_node = []
holders = []

sample_materials_for_holders = materials_name * holders_number
# random.shuffle(sample_materials_for_holders)
materials_number = len(materials_name)

for i in range(holders_number):
if not max_materials:
materials_name_for_holder = []
else:
materials_name_for_holder = sample_materials_for_holders[i * materials_number: (i + 1) * materials_number]
holders_node.append(LandGraphNode(str(uuid.uuid4()), f'holder{i}',
ResourceStorageUnit(
{
name: max(max_materials[name], 1) * wg.vertex_count
for name in materials_name_for_holder
}
)))
neighbour_platforms = rnd.choices(holders_node[:-1] + platforms, k=rnd.randint(1, len(holders_node[:-1] + platforms)))

neighbour_platforms_tmp = neighbour_platforms.copy()
for neighbour in neighbour_platforms:
if neighbour in holders_node[-1].neighbours:
neighbour_platforms_tmp.remove(neighbour)
neighbour_platforms = neighbour_platforms_tmp

# neighbour_edges = [(neighbour, rnd.uniform(1.0, 10.0), rnd.randint(wg.vertex_count, wg.vertex_count * 2))
# for neighbour in neighbour_platforms]
lengths = [i * 50 for i in range(1, len(neighbour_platforms) + 1)]
neighbour_edges = [(neighbour, lengths[i], wg.vertex_count * 2)
for i, neighbour in enumerate(neighbour_platforms)]
holders_node[-1].add_neighbours(neighbour_edges)

# vehicles_number = rnd.randint(7, 20)
vehicles_number = 20
holders.append(ResourceHolder(str(uuid.uuid4()), holders_node[-1].name,
vehicles=[
Vehicle(str(uuid.uuid4()), f'vehicle{j}',
[Material(name, name, count // 2)
for name, count in max_materials.items()])
for j in range(vehicles_number)
], node=holders_node[-1]))

lg = LandGraph(nodes=platforms + holders_node)
return LandscapeConfiguration(holders, lg)
Empty file.
6 changes: 6 additions & 0 deletions sampo/landscape_config/material_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def max_fill(mat_max: int, mat_available: int):
return mat_max - mat_available


def necessary_fill(mat_count: int, mat_available: int, mat_max: int):
return mat_count + mat_available - mat_max
Loading

0 comments on commit dc365c7

Please sign in to comment.