Solve optimizer #268
26 changed files with 653 additions and 173 deletions
|
@ -1,49 +0,0 @@
|
||||||
import FreeCAD
|
|
||||||
import FreeCADGui
|
|
||||||
from PySide import QtGui, QtCore
|
|
||||||
|
|
||||||
class DatumTool:
|
|
||||||
"""
|
|
||||||
A tool for creating datums in existing models
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self.active = False
|
|
||||||
|
|
||||||
def activate(self):
|
|
||||||
self.active = True
|
|
||||||
FreeCAD.Console.PrintMessage("Datum tool activatedn")
|
|
||||||
|
|
||||||
def deactivate(self):
|
|
||||||
self.active = False
|
|
||||||
FreeCAD.Console.PrintMessage("Datum tool deactivatedn")
|
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
|
||||||
if self.active:
|
|
||||||
# Create a datum at the position of the mouse click
|
|
||||||
pos = FreeCADGui.ActiveDocument.ActiveView.getCursorPos()
|
|
||||||
point = FreeCADGui.ActiveDocument.ActiveView.getPoint(pos)
|
|
||||||
datum = FreeCAD.ActiveDocument.addObject("Part::Datum", "Datum")
|
|
||||||
datum.Placement.Base = point
|
|
||||||
datum.ViewObject.ShapeColor = (0.0, 1.0, 0.0) # Set the color of the datum to green
|
|
||||||
FreeCAD.ActiveDocument.recompute()
|
|
||||||
|
|
||||||
class DatumCommand:
|
|
||||||
"""
|
|
||||||
A command for activating and deactivating the datum tool
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self.tool = DatumTool()
|
|
||||||
|
|
||||||
def Activated(self):
|
|
||||||
self.tool.activate()
|
|
||||||
FreeCADGui.ActiveDocument.ActiveView.addEventCallback("SoMouseButtonEvent", self.tool.mousePressEvent)
|
|
||||||
|
|
||||||
def Deactivated(self):
|
|
||||||
self.tool.deactivate()
|
|
||||||
FreeCADGui.ActiveDocument.ActiveView.removeEventCallback("SoMouseButtonEvent", self.tool.mousePressEvent)
|
|
||||||
|
|
||||||
def GetResources(self):
|
|
||||||
return {'Pixmap': 'path/to/icon.png', 'MenuText': 'Datum Tool', 'ToolTip': 'Creates datum elements in existing models'}
|
|
||||||
|
|
||||||
# Add the command to the Draft Workbench
|
|
||||||
FreeCADGui.addCommand('DatumCommand', DatumCommand())
|
|
11
freecad_workbench/freecad/robossembler/README.md
Normal file
11
freecad_workbench/freecad/robossembler/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Общее руководство работы с планировщиком сборки.
|
||||||
|
|
||||||
|
1. Открыть сборку. С помощью команд верстака Robossembler, используя команды "Create Assembly Parameters", "Create fastener set", "Create assembly sequence", произвести разметку элементов сборки. Произвести экспорт настроек с помощью функции
|
||||||
|
export assembly settings. На выходе получим файл assembly_settings.json
|
||||||
|
2. Запустить geometric_feasibility_predicate/main.py, указав в env.json значения cadFilePath (путь к сборке) и outPath (путь вывода). Рекомендуется хранить все файлы в одном месте с проектом На выходе получается файл adjacency_matrix.json
|
||||||
|
|
||||||
|
3. ( опционально) Запустить файл solve_optimizer , получить упрощенную матрицу смежности в виде reduced_adjacency_matrix.json
|
||||||
|
|
||||||
|
4. Произвести расчет последовательности доступным способом, напр. через get_sequences.json или с помощью asp.
|
||||||
|
|
||||||
|
5. (Если применено упрощение) Добавить исключенные компоненты с помощью соотв. функции.
|
|
@ -1,37 +0,0 @@
|
||||||
# -*- coding: utf8 -*-
|
|
||||||
|
|
||||||
#***************************************************************************
|
|
||||||
#* *
|
|
||||||
#* Copyright (c) 2020 kbwbe *
|
|
||||||
#* *
|
|
||||||
#* *
|
|
||||||
#* This program is free software; you can redistribute it and/or modify *
|
|
||||||
#* it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
||||||
#* as published by the Free Software Foundation; either version 2 of *
|
|
||||||
#* the License, or (at your option) any later version. *
|
|
||||||
#* for detail see the LICENCE text file. *
|
|
||||||
#* *
|
|
||||||
#* 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 Library General Public License for more details. *
|
|
||||||
#* *
|
|
||||||
#* You should have received a copy of the GNU Library General Public *
|
|
||||||
#* License along with this program; if not, write to the Free Software *
|
|
||||||
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
||||||
#* USA *
|
|
||||||
#* *
|
|
||||||
#***************************************************************************
|
|
||||||
|
|
||||||
import FreeCAD
|
|
||||||
|
|
||||||
|
|
||||||
if FreeCAD.GuiUp:
|
|
||||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
||||||
from DraftGui import translate
|
|
||||||
else:
|
|
||||||
def QT_TRANSLATE_NOOP(context, text):
|
|
||||||
return text
|
|
||||||
|
|
||||||
def translate(context, text):
|
|
||||||
return text
|
|
|
@ -37,4 +37,4 @@ def draw_graph_with_thumbnails(G, image_folder):
|
||||||
def main():
|
def main():
|
||||||
sequences = load_sequences('valid_sequences.json')
|
sequences = load_sequences('valid_sequences.json')
|
||||||
G = create_graph(sequences)
|
G = create_graph(sequences)
|
||||||
draw_graph_with_thumbnails(G, '/home/markvoltov/GitProjects/framework/test_models/img')
|
draw_graph_with_thumbnails(G, 'path_to_img')
|
||||||
|
|
28
freecad_workbench/freecad/robossembler/asm_main.py
Normal file
28
freecad_workbench/freecad/robossembler/asm_main.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import FreeCAD as App
|
||||||
|
from .geometric_feasibility_predicate.main import main as asm_analysis
|
||||||
|
from get_sequences import process_adjacency_data
|
||||||
|
from valid_sequences import filter_valid_sequences
|
||||||
|
from solve_optimizer import restore_full_sequence # Убедитесь, что эта функция импортирована
|
||||||
|
from constraints_operator import collect_assembly_settings
|
||||||
|
|
||||||
|
def main(flag):
|
||||||
|
# Выполняем анализ сборки и получаем необходимые данные
|
||||||
|
#flag используется для того, чтобы выбирать между обычным и оптимизированным вариантом работы
|
||||||
|
intersection_geometry, sequences, topologyMatrix = asm_analysis()
|
||||||
|
adjacency_matrix = topologyMatrix.matrix
|
||||||
|
assembly_settings = collect_assembly_settings()
|
||||||
|
|
||||||
|
# Упрощаем матрицу смежности
|
||||||
|
if flag:
|
||||||
|
simplified_matrix = simplify_adjacency_matrix(assembly_settings, adjacency_matrix)
|
||||||
|
all_parts, graph, first_detail, leaf_nodes, all_sequences = process_adjacency_data(simplified_matrix)
|
||||||
|
else:
|
||||||
|
all_parts, graph, first_detail, leaf_nodes, all_sequences = process_adjacency_data(adjacency_matrix)
|
||||||
|
|
||||||
|
# Фильтруем допустимые последовательности
|
||||||
|
valid_sequences = filter_valid_sequences(adjacency_matrix, sequences, assembly_settings)
|
||||||
|
full_sequence = restore_full_sequence(assembly_settings, all_sequences)
|
||||||
|
|
||||||
|
return full_sequence
|
||||||
|
|
||||||
|
main()
|
|
@ -35,8 +35,9 @@ def draw_graph(G):
|
||||||
plt.show()
|
plt.show()
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sequence = load_assembly_sequence('assembly_sequence.json')
|
sequence = load_assembly_sequence('path_to_adjacency_matrix.json')
|
||||||
assembly_graph = create_assembly_graph(sequence)
|
assembly_graph = create_assembly_graph(sequence)
|
||||||
|
|
||||||
draw_graph(assembly_graph)
|
draw_graph(assembly_graph)
|
||||||
|
|
||||||
|
# main()
|
||||||
|
|
|
@ -3,7 +3,7 @@ import FreeCADGui as Gui
|
||||||
import Draft
|
import Draft
|
||||||
import json
|
import json
|
||||||
|
|
||||||
sequence_file = '/home/markvoltov/GitProjects/framework/freecad_workbench/freecad/robossembler/sequences.json'
|
sequence_file = 'path_to_sequencesjson'
|
||||||
|
|
||||||
def load_assembly_sequence(filepath):
|
def load_assembly_sequence(filepath):
|
||||||
with open(filepath, 'r') as file:
|
with open(filepath, 'r') as file:
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
import networkx as nx
|
||||||
|
|
||||||
|
|
||||||
|
class GraphProcessor:
|
||||||
|
graph = None
|
||||||
|
|
||||||
|
def __init__(self, adjacency_matrix):
|
||||||
|
self.adjacency_matrix = adjacency_matrix
|
||||||
|
self.graph = self.load_graph_from_data()
|
||||||
|
|
||||||
|
def load_graph_from_data(self):
|
||||||
|
G = nx.Graph()
|
||||||
|
|
||||||
|
for part1, neighbors in self.adjacency_matrix.items():
|
||||||
|
for neighbor in neighbors:
|
||||||
|
G.add_edge(part1, neighbor)
|
||||||
|
|
||||||
|
return G
|
||||||
|
|
||||||
|
|
||||||
|
class EdgeBetweensClustering:
|
||||||
|
def __init__(self, graph):
|
||||||
|
self.graph = graph.copy()
|
||||||
|
self.clusters = []
|
||||||
|
|
||||||
|
def cluster(self):
|
||||||
|
while self.graph.number_of_edges() > 0:
|
||||||
|
edge_betweens = nx.edge_betweenness_centrality(self.graph)
|
||||||
|
max_betweens_edge = max(edge_betweens, key=edge_betweens.get)
|
||||||
|
self.graph.remove_edge(*max_betweens_edge)
|
||||||
|
components = list(nx.connected_components(self.graph))
|
||||||
|
if components not in self.clusters:
|
||||||
|
self.clusters.append(components)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_clusters(self):
|
||||||
|
return self.clusters
|
||||||
|
|
||||||
|
|
||||||
|
class ClusterisationSequenceUseCase:
|
||||||
|
def call(self, adjacency_matrix):
|
||||||
|
graph_processor = GraphProcessor(adjacency_matrix)
|
||||||
|
G = graph_processor.load_graph_from_data()
|
||||||
|
ebc = EdgeBetweensClustering(G)
|
||||||
|
ebc.cluster()
|
||||||
|
clusters = ebc.get_clusters()
|
||||||
|
|
||||||
|
for i in range(len(clusters)):
|
||||||
|
for j in range(len(clusters[i])):
|
||||||
|
clusters[i][j] = list(clusters[i][j])
|
||||||
|
|
||||||
|
# Создание списка последовательностей сборки
|
||||||
|
assembly_sequences = []
|
||||||
|
for cluster in clusters:
|
||||||
|
sequence = []
|
||||||
|
for component in cluster:
|
||||||
|
sequence.extend(component)
|
||||||
|
assembly_sequences.append(sequence)
|
||||||
|
|
||||||
|
return assembly_sequences
|
||||||
|
|
||||||
|
|
||||||
|
# # Вызов функции
|
||||||
|
# adjacency_matrix = {
|
||||||
|
# 'body_down': ['sol_gear', 'planet_gear', 'planet_gear003', 'planet_gear004', 'planet_gear005', 'planet_gear002', 'body_up', 'bolt', 'bolt2', 'bolt3', 'bolt4'],
|
||||||
|
# 'sol_gear': ['body_down', 'output_shaft', 'planet_gear', 'planet_gear003', 'planet_gear004', 'planet_gear005', 'planet_gear002'],
|
||||||
|
# 'output_shaft': ['sol_gear', 'planet_gear', 'planet_gear003', 'planet_gear004', 'planet_gear005', 'planet_gear002', 'body_up'],
|
||||||
|
# 'planet_gear': ['body_down', 'sol_gear', 'output_shaft'],
|
||||||
|
# 'planet_gear003': ['body_down', 'sol_gear', 'output_shaft'],
|
||||||
|
# 'planet_gear004': ['body_down', 'sol_gear', 'output_shaft'],
|
||||||
|
# 'planet_gear005': ['body_down', 'sol_gear', 'output_shaft'],
|
||||||
|
# 'planet_gear002': ['body_down', 'sol_gear', 'output_shaft'],
|
||||||
|
# 'body_up': ['body_down', 'output_shaft', 'bolt', 'bolt2', 'bolt3', 'bolt4'],
|
||||||
|
# 'bolt': ['body_down', 'body_up'],
|
||||||
|
# 'bolt2': ['body_down', 'body_up'],
|
||||||
|
# 'bolt3': ['body_down', 'body_up'],
|
||||||
|
# 'bolt4': ['body_down', 'body_up']
|
||||||
|
# }
|
||||||
|
# print(adjacency_matrix)
|
||||||
|
# use_case = ClusterisationSequenceUseCase()
|
||||||
|
# assembly_sequences = use_case.call(adjacency_matrix)
|
||||||
|
# print(assembly_sequences)
|
|
@ -135,6 +135,50 @@ def export_assembly_settings():
|
||||||
with open(save_path, "w") as f:
|
with open(save_path, "w") as f:
|
||||||
json.dump(data, f, indent=4)
|
json.dump(data, f, indent=4)
|
||||||
|
|
||||||
|
#для работы с данными как с переменной
|
||||||
|
def collect_assembly_settings():
|
||||||
|
doc = App.activeDocument()
|
||||||
|
if not doc:
|
||||||
|
return None
|
||||||
|
|
||||||
|
assembly_settings_folder = None
|
||||||
|
for obj in doc.Objects:
|
||||||
|
if obj.Name == "Assembly_Settings":
|
||||||
|
assembly_settings_folder = obj
|
||||||
|
break
|
||||||
|
|
||||||
|
if not assembly_settings_folder:
|
||||||
|
return None
|
||||||
|
|
||||||
|
assembly_settings = []
|
||||||
|
for obj in assembly_settings_folder.Group:
|
||||||
|
if hasattr(obj, "Type"):
|
||||||
|
obj_dict = {"Name": obj.Name}
|
||||||
|
if obj.Type == "fastener_set":
|
||||||
|
fasteners = [part.Label for part in obj.Fasteners]
|
||||||
|
obj_dict.update({
|
||||||
|
"Type": "fastener_set",
|
||||||
|
"Parent": obj.Parent.Label,
|
||||||
|
"Child": obj.Child.Label,
|
||||||
|
"Fasteners": fasteners
|
||||||
|
})
|
||||||
|
elif obj.Type == "asm_sequence":
|
||||||
|
obj_dict.update({
|
||||||
|
"Type": "asm_sequence",
|
||||||
|
"Parent": obj.Parent.Label,
|
||||||
|
"Child": obj.Child.Label
|
||||||
|
})
|
||||||
|
elif obj.Type == "clearance":
|
||||||
|
partnames = [part.Label for part in obj.PartName]
|
||||||
|
obj_dict.update({
|
||||||
|
"Type": "clearance",
|
||||||
|
"PartName": partnames,
|
||||||
|
"MaxClearance": obj.MaxClearance
|
||||||
|
})
|
||||||
|
assembly_settings.append(obj_dict)
|
||||||
|
|
||||||
|
return assembly_settings
|
||||||
|
|
||||||
# create_fastener_set()
|
# create_fastener_set()
|
||||||
# create_assembly_sequence()
|
# create_assembly_sequence()
|
||||||
# create_clearance_constraint()
|
# create_clearance_constraint()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"cadFilePath": "path/to/file",
|
"cadFilePath": "path_to_cad_file",
|
||||||
"outPath": "out/path",
|
"outPath": "out_path",
|
||||||
"objectIndentation": 0
|
"objectIndentation": 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ class AllSequences:
|
||||||
for el in v:
|
for el in v:
|
||||||
adj[inc][el - 1] = 1
|
adj[inc][el - 1] = 1
|
||||||
inc += 1
|
inc += 1
|
||||||
return self.find_all_sequences(adj)
|
self.find_all_sequences(adj)
|
||||||
|
|
||||||
|
|
||||||
class VectorModel:
|
class VectorModel:
|
||||||
|
@ -580,8 +580,8 @@ class CadAdjacencyMatrix:
|
||||||
collisionResult: int = int(
|
collisionResult: int = int(
|
||||||
part.Shape.distToShape(nextPart.Shape)[0]
|
part.Shape.distToShape(nextPart.Shape)[0]
|
||||||
)
|
)
|
||||||
print(collisionResult)
|
# print(collisionResult)
|
||||||
print("collisionResult")
|
# print("collisionResult")
|
||||||
if collisionResult == 0:
|
if collisionResult == 0:
|
||||||
matrix[part.Label].append(nextPart.Label)
|
matrix[part.Label].append(nextPart.Label)
|
||||||
|
|
||||||
|
@ -708,68 +708,82 @@ class ExitFreeCadUseCase:
|
||||||
# FreeCadRepository().obj
|
# FreeCadRepository().obj
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
|
#функция, проверяющая, открывается ли программа через консоль или через верстак freecad.
|
||||||
|
def get_paths():
|
||||||
|
if 'FreeCAD' in globals():
|
||||||
|
active_doc = FreeCAD.activeDocument()
|
||||||
|
if active_doc:
|
||||||
|
cadFilePath = active_doc.FileName
|
||||||
|
outPath = os.path.dirname(cadFilePath)
|
||||||
|
else:
|
||||||
|
raise Exception("Нет активного документа в FreeCAD.")
|
||||||
|
else:
|
||||||
|
with open('env.json', 'r', encoding='utf-8') as file:
|
||||||
|
env_data = json.load(file)
|
||||||
|
cadFilePath = env_data.get('cadFilePath')
|
||||||
|
outPath = env_data.get('outPath')
|
||||||
|
if not cadFilePath or not outPath:
|
||||||
|
raise Exception("Не найдены cadFilePath или outPath в env.json.")
|
||||||
|
|
||||||
|
return cadFilePath, outPath
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
env = FS.readJSON("env.json")
|
if 'FreeCAD' in globals():
|
||||||
cadFilePath = str(env["cadFilePath"])
|
active_doc = FreeCAD.activeDocument()
|
||||||
outPath = str(env["outPath"])
|
if active_doc:
|
||||||
objectIndentation = float(env["objectIndentation"])
|
cadFilePath = active_doc.FileName
|
||||||
|
outPath = os.path.dirname(cadFilePath)
|
||||||
|
else:
|
||||||
|
# raise Exception("Нет активного документа в FreeCAD.")
|
||||||
|
# else:
|
||||||
|
env = FS.readJSON("env.json")
|
||||||
|
print(env)
|
||||||
|
cadFilePath = str(env["cadFilePath"])
|
||||||
|
outPath = str(env["outPath"])
|
||||||
|
objectIndentation = float(env["objectIndentation"])
|
||||||
|
|
||||||
if cadFilePath == None:
|
if cadFilePath == None:
|
||||||
return TypeError("CadFile not found env.json")
|
return TypeError("CadFile not found env.json")
|
||||||
App.open("" + cadFilePath)
|
App.open("" + cadFilePath)
|
||||||
|
|
||||||
# isAllObjectSolidsCheckUseCase = IsAllObjectSolidsCheckUseCase.call()
|
isAllObjectSolidsCheckUseCase = IsAllObjectSolidsCheckUseCase.call()
|
||||||
|
|
||||||
# if isAllObjectSolidsCheckUseCase != None:
|
if isAllObjectSolidsCheckUseCase != None:
|
||||||
# FS.writeFile(isAllObjectSolidsCheckUseCase.toString(), outPath, 'error.json')
|
FS.writeFile(isAllObjectSolidsCheckUseCase.toString(), outPath, 'error.json')
|
||||||
# ExitFreeCadUseCase.call()
|
ExitFreeCadUseCase.call()
|
||||||
# return
|
return
|
||||||
|
|
||||||
|
FreeCAD.open(cadFilePath)
|
||||||
|
|
||||||
|
|
||||||
# checkObjectHasTouchesUseCase = CheckObjectHasTouchesUseCase.call(objectIndentation)
|
|
||||||
|
|
||||||
# if checkObjectHasTouchesUseCase != None:
|
|
||||||
# FS.writeFile(checkObjectHasTouchesUseCase.toString(), outPath, 'error.json')
|
|
||||||
# ExitFreeCadUseCase.call()
|
|
||||||
# return
|
|
||||||
|
|
||||||
topologyMatrix = CadAdjacencyMatrix().matrixBySurfaces()
|
topologyMatrix = CadAdjacencyMatrix().matrixBySurfaces()
|
||||||
import json
|
|
||||||
|
|
||||||
sequences = json.dumps(
|
sequences = {
|
||||||
{"sequences": AllSequences(topologyMatrix.matrix).adj_matrix_names},
|
"sequences": AllSequences(topologyMatrix.matrix).adj_matrix_names
|
||||||
ensure_ascii=False,
|
}
|
||||||
indent=4,
|
|
||||||
)
|
|
||||||
matrix = topologyMatrix.matrix
|
matrix = topologyMatrix.matrix
|
||||||
contacts = matrixGetUniqueContact(matrix)
|
contacts = matrixGetUniqueContact(matrix)
|
||||||
intersection_geometry = {"status": True, "recalculations": None}
|
intersection_geometry = {"status": True, "recalculations": None}
|
||||||
|
|
||||||
for el in contacts:
|
for el in contacts:
|
||||||
child = App.ActiveDocument.getObjectsByLabel(el.get("child"))[0]
|
child = App.ActiveDocument.getObjectsByLabel(el.get("child"))[0]
|
||||||
parent = App.ActiveDocument.getObjectsByLabel(el.get("parent"))[0]
|
parent = App.ActiveDocument.getObjectsByLabel(el.get("parent"))[0]
|
||||||
area = IntersectionComputedUseCase.call([child, parent])
|
area = IntersectionComputedUseCase.call([child, parent])
|
||||||
if area != 0.0:
|
if area != 0.0:
|
||||||
if intersection_geometry.get("recalculations") == None:
|
if intersection_geometry.get("recalculations") is None:
|
||||||
intersection_geometry["status"] = False
|
intersection_geometry["status"] = False
|
||||||
intersection_geometry["recalculations"] = []
|
intersection_geometry["recalculations"] = []
|
||||||
intersection_geometry["recalculations"].append(
|
intersection_geometry["recalculations"].append(
|
||||||
{"area": area, "connect": el.get("child") + " " + el.get("parent")}
|
{"area": area, "connect": el.get("child") + " " + el.get("parent")}
|
||||||
)
|
)
|
||||||
|
# print(intersection_geometry, sequences, topologyMatrix.to_dict())
|
||||||
|
return intersection_geometry, sequences, topologyMatrix.to_dict()
|
||||||
|
|
||||||
FS.writeFile(
|
# ExitFreeCadUseCase.call() Сейчас пока не нужна
|
||||||
json.dumps(intersection_geometry, ensure_ascii=False, indent=4),
|
# return intersection_geometry, sequences, topologyMatrix.to_dict()
|
||||||
outPath,
|
|
||||||
"intersection_geometry.json",
|
|
||||||
)
|
|
||||||
FS.writeFile(sequences, outPath, "sequences.json")
|
|
||||||
|
|
||||||
FS.writeFile(
|
|
||||||
json.dumps(topologyMatrix.to_dict(), ensure_ascii=False, indent=4),
|
|
||||||
outPath,
|
|
||||||
"adjacency_matrix.json",
|
|
||||||
)
|
|
||||||
ExitFreeCadUseCase.call()
|
|
||||||
|
|
||||||
|
|
||||||
#main()
|
# main()
|
||||||
|
|
|
@ -42,20 +42,36 @@ def save_sequences(sequences, file_path):
|
||||||
with open(file_path, 'w') as file:
|
with open(file_path, 'w') as file:
|
||||||
json.dump(sequences, file, indent=4)
|
json.dump(sequences, file, indent=4)
|
||||||
|
|
||||||
data = load_data('adjacency_matrix.json')
|
# data = load_data('path_to_adjacency_matrix.json')
|
||||||
constraints = load_constraints('constraints.json')
|
# # constraints = load_constraints('constraints.json')
|
||||||
all_parts = data['allParts']
|
# all_parts = data['allParts']
|
||||||
graph = create_graph(data)
|
# print(all_parts)
|
||||||
first_detail = data['firstDetail']
|
# graph = create_graph(data)
|
||||||
leaf_nodes = find_leaf_nodes(graph, first_detail)
|
# first_detail = data['firstDetail']
|
||||||
|
# leaf_nodes = find_leaf_nodes(graph, first_detail)
|
||||||
|
|
||||||
all_sequences = []
|
# all_sequences = []
|
||||||
for leaf in leaf_nodes:
|
# for leaf in leaf_nodes:
|
||||||
paths = find_all_paths(graph, leaf, first_detail)
|
# paths = find_all_paths(graph, leaf, first_detail)
|
||||||
for path in paths:
|
# for path in paths:
|
||||||
if set(path) == set(all_parts) and is_valid_sequence(path, constraints):
|
# if set(path) == set(all_parts) and is_valid_sequence(path, constraints):
|
||||||
all_sequences.append(path)
|
# all_sequences.append(path)
|
||||||
|
|
||||||
save_sequences(all_sequences, 'valid_sequences.json')
|
# save_sequences(all_sequences, 'valid_sequences.json')
|
||||||
|
|
||||||
print(f"Найдено {len(all_sequences)} допустимых последовательностей сборки.")
|
# print(f"Найдено {len(all_sequences)} допустимых последовательностей сборки.")
|
||||||
|
|
||||||
|
def process_adjacency_data(topology_matrix):
|
||||||
|
all_parts = topology_matrix['allParts']
|
||||||
|
graph = create_graph(topology_matrix)
|
||||||
|
first_detail = topology_matrix['firstDetail']
|
||||||
|
leaf_nodes = find_leaf_nodes(graph, first_detail)
|
||||||
|
|
||||||
|
all_sequences = []
|
||||||
|
for leaf in leaf_nodes:
|
||||||
|
paths = find_all_paths(graph, leaf, first_detail)
|
||||||
|
for path in paths:
|
||||||
|
if set(path) == set(all_parts) and is_valid_sequence(path, constraints):
|
||||||
|
all_sequences.append(path)
|
||||||
|
|
||||||
|
return all_parts, graph, first_detail, leaf_nodes, all_sequences
|
||||||
|
|
|
@ -3,7 +3,7 @@ import networkx as nx
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
# Загружаем данные из файла
|
# Загружаем данные из файла
|
||||||
with open('adjacency_matrix.json', 'r') as file:
|
with open('path_tp_simplified_adjacency_matrix.json', 'r') as file:
|
||||||
data = json.load(file)
|
data = json.load(file)
|
||||||
|
|
||||||
# Создаем пустой граф
|
# Создаем пустой граф
|
||||||
|
|
|
@ -2,7 +2,6 @@ import os
|
||||||
import FreeCADGui as Gui
|
import FreeCADGui as Gui
|
||||||
import FreeCAD as App
|
import FreeCAD as App
|
||||||
|
|
||||||
from .TranslateUtils import translate
|
|
||||||
from . import ICONPATH, TRANSLATIONSPATH, Frames
|
from . import ICONPATH, TRANSLATIONSPATH, Frames
|
||||||
from .version import __version__
|
from .version import __version__
|
||||||
|
|
||||||
|
@ -15,6 +14,14 @@ __url__ = ["https://robossembler.org"]
|
||||||
__status__ = 'development'
|
__status__ = 'development'
|
||||||
|
|
||||||
|
|
||||||
|
def QT_TRANSLATE_NOOP(ctx, txt):
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
|
def translate(ctx, txt):
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
class Robossembler(Gui.Workbench):
|
class Robossembler(Gui.Workbench):
|
||||||
"""
|
"""
|
||||||
class which gets initiated at startup of the gui
|
class which gets initiated at startup of the gui
|
||||||
|
|
|
@ -105,7 +105,7 @@ def main():
|
||||||
# args.print_helper()
|
# args.print_helper()
|
||||||
# if (aspDir[aspDir.__len__() - 1] != '/'):
|
# if (aspDir[aspDir.__len__() - 1] != '/'):
|
||||||
# aspDir += '/'
|
# aspDir += '/'
|
||||||
aspDir = "/home/markvoltov/GitProjects/framework/test_models/"
|
aspDir = "path_to_models/"
|
||||||
sequences = FS.readJSON(aspDir + 'sequences.json').get('sequences')
|
sequences = FS.readJSON(aspDir + 'sequences.json').get('sequences')
|
||||||
|
|
||||||
assemblyDirNormalize = []
|
assemblyDirNormalize = []
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
{
|
||||||
|
"allParts": [
|
||||||
|
"body_down",
|
||||||
|
"body_up",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft",
|
||||||
|
"planet_gear",
|
||||||
|
"planet_gear003",
|
||||||
|
"planet_gear004",
|
||||||
|
"planet_gear005",
|
||||||
|
"planet_gear002",
|
||||||
|
"bolt",
|
||||||
|
"bolt2",
|
||||||
|
"bolt3",
|
||||||
|
"bolt4"
|
||||||
|
],
|
||||||
|
"matrix": {
|
||||||
|
"body_down": [
|
||||||
|
"sol_gear",
|
||||||
|
"planet_gear",
|
||||||
|
"planet_gear003",
|
||||||
|
"planet_gear004",
|
||||||
|
"planet_gear005",
|
||||||
|
"planet_gear002",
|
||||||
|
"body_up"
|
||||||
|
],
|
||||||
|
"body_up": [
|
||||||
|
"body_down",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"sol_gear": [
|
||||||
|
"body_down",
|
||||||
|
"output_shaft",
|
||||||
|
"planet_gear",
|
||||||
|
"planet_gear003",
|
||||||
|
"planet_gear004",
|
||||||
|
"planet_gear005",
|
||||||
|
"planet_gear002"
|
||||||
|
],
|
||||||
|
"output_shaft": [
|
||||||
|
"sol_gear",
|
||||||
|
"planet_gear",
|
||||||
|
"planet_gear003",
|
||||||
|
"planet_gear004",
|
||||||
|
"planet_gear005",
|
||||||
|
"planet_gear002",
|
||||||
|
"body_up"
|
||||||
|
],
|
||||||
|
"planet_gear": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"planet_gear003": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"planet_gear004": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"planet_gear005": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"planet_gear002": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
175
freecad_workbench/freecad/robossembler/solve_optimizer.py
Normal file
175
freecad_workbench/freecad/robossembler/solve_optimizer.py
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
'''
|
||||||
|
Файл, содержащий скрипт для обработки и упрощения матрицы смежности. Запускается через команду в основном меню верстака freecad
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import json
|
||||||
|
import FreeCAD as App
|
||||||
|
from geometric_feasibility_predicate.main import main as asm_analysis
|
||||||
|
from constraints_operator import collect_assembly_settings
|
||||||
|
from clusterisation_sequences import ClusterisationSequenceUseCase
|
||||||
|
|
||||||
|
|
||||||
|
# === Для работы с json-файлами. Работает. ===
|
||||||
|
def simplify_adjacency_matrix_json(assembly_file, adjacency_file, output_file):
|
||||||
|
def load_json(file_path):
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
return json.load(file)
|
||||||
|
|
||||||
|
def save_json(file_path, data):
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as file:
|
||||||
|
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
assembly_settings = load_json('/assembly_settings.json')
|
||||||
|
adjacency_matrix = load_json('/adjacency_matrix.json')
|
||||||
|
|
||||||
|
fasteners = set()
|
||||||
|
for item in assembly_settings:
|
||||||
|
if item.get("Type") == "fastener_set":
|
||||||
|
fasteners.add(item["Parent"])
|
||||||
|
fasteners.add(item["Child"])
|
||||||
|
|
||||||
|
simplified_matrix = {
|
||||||
|
"allParts": [],
|
||||||
|
"matrix": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for part in adjacency_matrix["allParts"]:
|
||||||
|
if part not in fasteners:
|
||||||
|
simplified_matrix["allParts"].append(part)
|
||||||
|
neighbors = [
|
||||||
|
neighbor for neighbor in adjacency_matrix["matrix"].get(part, [])
|
||||||
|
if neighbor not in fasteners
|
||||||
|
]
|
||||||
|
if neighbors or part not in fasteners:
|
||||||
|
simplified_matrix["matrix"][part] = neighbors
|
||||||
|
|
||||||
|
save_json(output_file, simplified_matrix)
|
||||||
|
|
||||||
|
|
||||||
|
# simplify_adjacency_matrix('assembly_settings.json', 'adjacency_matrix.json', 'simplified_adjacency_matrix.json')
|
||||||
|
|
||||||
|
|
||||||
|
def restore_full_sequence_json(assembly_file, sequence_file, output_file):
|
||||||
|
def load_json(file_path):
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
return json.load(file)
|
||||||
|
|
||||||
|
def save_json(file_path, data):
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as file:
|
||||||
|
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
assembly_settings = load_json(assembly_file)
|
||||||
|
sequence = load_json(sequence_file)
|
||||||
|
|
||||||
|
full_sequence = []
|
||||||
|
sequence_set = set(sequence)
|
||||||
|
|
||||||
|
for item in sequence:
|
||||||
|
full_sequence.append(item)
|
||||||
|
for setting in assembly_settings:
|
||||||
|
if setting.get("Type") == "fastener_set":
|
||||||
|
parent = setting["Parent"]
|
||||||
|
child = setting["Child"]
|
||||||
|
if parent in sequence_set and child in sequence_set:
|
||||||
|
full_sequence.append(setting["Fasteners"])
|
||||||
|
|
||||||
|
save_json(output_file, full_sequence)
|
||||||
|
|
||||||
|
|
||||||
|
# restore_full_sequence('assembly_settings.json', 'sequence.json', 'full_sequence.json')
|
||||||
|
|
||||||
|
|
||||||
|
# ==== Для работы с внутренними переменными
|
||||||
|
|
||||||
|
def simplify_adjacency_matrix(assembly_settings, adjacency_matrix):
|
||||||
|
fasteners = set()
|
||||||
|
for item in assembly_settings:
|
||||||
|
if item.get("Type") == "fastener_set":
|
||||||
|
fasteners.add(item["Parent"])
|
||||||
|
fasteners.add(item["Child"])
|
||||||
|
|
||||||
|
simplified_matrix = {
|
||||||
|
"allParts": [],
|
||||||
|
"matrix": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for part in adjacency_matrix["allParts"]:
|
||||||
|
# Добавляем все детали, включая крепеж, в simplified_matrix["allParts"]
|
||||||
|
simplified_matrix["allParts"].append(part)
|
||||||
|
|
||||||
|
# Получаем соседей, исключая только крепежные элементы
|
||||||
|
neighbors = [
|
||||||
|
neighbor for neighbor in adjacency_matrix["matrix"].get(part, [])
|
||||||
|
if neighbor not in fasteners
|
||||||
|
]
|
||||||
|
|
||||||
|
# Если у части есть соседи, добавляем их в матрицу
|
||||||
|
if neighbors:
|
||||||
|
simplified_matrix["matrix"][part] = neighbors
|
||||||
|
|
||||||
|
return simplified_matrix
|
||||||
|
|
||||||
|
|
||||||
|
def restore_full_sequence(assembly_settings, sequences):
|
||||||
|
full_sequences = []
|
||||||
|
|
||||||
|
for sequence in sequences:
|
||||||
|
full_sequence = []
|
||||||
|
sequence_set = set(sequence)
|
||||||
|
|
||||||
|
# Сначала добавляем все детали в полную последовательность
|
||||||
|
for item in sequence:
|
||||||
|
full_sequence.append(item)
|
||||||
|
|
||||||
|
# Теперь добавляем крепежные элементы после элемента с наибольшим порядковым номером
|
||||||
|
for setting in assembly_settings:
|
||||||
|
if setting.get("Type") == "fastener_set":
|
||||||
|
parent = setting["Parent"]
|
||||||
|
child = setting["Child"]
|
||||||
|
fasteners = setting["Fasteners"]
|
||||||
|
|
||||||
|
# Проверяем, если родитель и ребенок в последовательности
|
||||||
|
if (parent in sequence_set) and (child in sequence_set):
|
||||||
|
# Находим индексы родителя и ребенка в полной последовательности
|
||||||
|
parent_index = full_sequence.index(parent)
|
||||||
|
child_index = full_sequence.index(child)
|
||||||
|
|
||||||
|
# Находим максимальный индекс между родителем и ребенком
|
||||||
|
max_index = max(parent_index, child_index)
|
||||||
|
|
||||||
|
# Проверяем, содержатся ли крепежные элементы уже в последовательности
|
||||||
|
if not any(fastener in full_sequence for fastener in fasteners):
|
||||||
|
# Добавляем крепежные элементы после элемента child
|
||||||
|
full_sequence[max_index + 1:max_index + 1] = fasteners
|
||||||
|
|
||||||
|
full_sequences.append(full_sequence)
|
||||||
|
|
||||||
|
return full_sequences
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
App.open('/home/markvoltov/GitProjects/framework/test_models/test_reductor.FCStd')
|
||||||
|
if App.ActiveDocument:
|
||||||
|
intersection_geometry, sequences, topologyMatrix = asm_analysis()
|
||||||
|
|
||||||
|
assembly_settings = collect_assembly_settings()
|
||||||
|
print(assembly_settings)
|
||||||
|
simplified_matrix = simplify_adjacency_matrix(assembly_settings, topologyMatrix)
|
||||||
|
|
||||||
|
assembly_sequences = ClusterisationSequenceUseCase().call(topologyMatrix['matrix'])
|
||||||
|
# print('Последовательности 1', assembly_sequences)
|
||||||
|
assembly_sequences_simplified= ClusterisationSequenceUseCase().call(simplified_matrix['matrix'])
|
||||||
|
# print('Последовательности 2',assembly_sequences_simplified)
|
||||||
|
assembly_sequences_restored = restore_full_sequence(assembly_settings, assembly_sequences_simplified)
|
||||||
|
print('Последовательности 3',assembly_sequences_restored)
|
||||||
|
else:
|
||||||
|
print('Ошибка. Нет активного документа!')
|
||||||
|
|
||||||
|
main()
|
6
freecad_workbench/freecad/robossembler/test.py
Normal file
6
freecad_workbench/freecad/robossembler/test.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
adj_matrix = {
|
||||||
|
"A": ["B", "C"],
|
||||||
|
"B": ["C"],
|
||||||
|
"C": []}
|
||||||
|
|
||||||
|
from geometric_feasibility_predicate.main import main as asm_analysis
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
|
@ -15,22 +15,29 @@ def save_sequences(sequences, file_path):
|
||||||
with open(file_path, 'w') as file:
|
with open(file_path, 'w') as file:
|
||||||
json.dump(sequences, file, indent=4)
|
json.dump(sequences, file, indent=4)
|
||||||
|
|
||||||
# Load data from files
|
# adjacency_matrix = load_data('adjacency_matrix.json')
|
||||||
adjacency_matrix = load_data('adjacency_matrix.json')
|
# constraints = load_data('constraints.json')
|
||||||
constraints = load_data('constraints.json')
|
# sequences = load_data('sequences.json')
|
||||||
sequences = load_data('sequences.json')
|
|
||||||
|
|
||||||
# Get all parts and first detail
|
# all_parts = adjacency_matrix['allParts']
|
||||||
all_parts = adjacency_matrix['allParts']
|
# first_detail = adjacency_matrix['firstDetail']
|
||||||
first_detail = adjacency_matrix['firstDetail']
|
|
||||||
|
|
||||||
# Filter valid sequences
|
# valid_sequences = []
|
||||||
valid_sequences = []
|
# for sequence in sequences:
|
||||||
for sequence in sequences:
|
# if len(set(sequence)) == len(set(all_parts)): #and is_valid_sequence(sequence, constraints):
|
||||||
if len(set(sequence)) == len(set(all_parts)): #and is_valid_sequence(sequence, constraints):
|
# valid_sequences.append(sequence)
|
||||||
valid_sequences.append(sequence)
|
|
||||||
|
|
||||||
# Save valid sequences to file
|
# save_sequences(valid_sequences, 'valid_sequences.json')
|
||||||
save_sequences(valid_sequences, 'valid_sequences.json')
|
|
||||||
|
|
||||||
print(f"Найдено {len(valid_sequences)} допустимых последовательностей сборки.")
|
# print(f"Найдено {len(valid_sequences)} допустимых последовательностей сборки.")
|
||||||
|
|
||||||
|
def filter_valid_sequences(adjacency_matrix, sequences, constraints):
|
||||||
|
all_parts = adjacency_matrix['allParts']
|
||||||
|
first_detail = adjacency_matrix['firstDetail']
|
||||||
|
|
||||||
|
valid_sequences = []
|
||||||
|
for sequence in sequences:
|
||||||
|
if len(set(sequence)) == len(set(all_parts)):
|
||||||
|
valid_sequences.append(sequence)
|
||||||
|
|
||||||
|
return valid_sequences
|
101
test_models/adjacency_matrix.json
Normal file
101
test_models/adjacency_matrix.json
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
{
|
||||||
|
"allParts": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft",
|
||||||
|
"planet_gear",
|
||||||
|
"planet_gear003",
|
||||||
|
"planet_gear004",
|
||||||
|
"planet_gear005",
|
||||||
|
"planet_gear002",
|
||||||
|
"body_up",
|
||||||
|
"bolt",
|
||||||
|
"bolt2",
|
||||||
|
"bolt3",
|
||||||
|
"bolt4"
|
||||||
|
],
|
||||||
|
"firstDetail": "body_down",
|
||||||
|
"matrix": {
|
||||||
|
"body_down": [
|
||||||
|
"sol_gear",
|
||||||
|
"planet_gear",
|
||||||
|
"planet_gear003",
|
||||||
|
"planet_gear004",
|
||||||
|
"planet_gear005",
|
||||||
|
"planet_gear002",
|
||||||
|
"body_up",
|
||||||
|
"bolt",
|
||||||
|
"bolt2",
|
||||||
|
"bolt3",
|
||||||
|
"bolt4"
|
||||||
|
],
|
||||||
|
"sol_gear": [
|
||||||
|
"body_down",
|
||||||
|
"output_shaft",
|
||||||
|
"planet_gear",
|
||||||
|
"planet_gear003",
|
||||||
|
"planet_gear004",
|
||||||
|
"planet_gear005",
|
||||||
|
"planet_gear002"
|
||||||
|
],
|
||||||
|
"output_shaft": [
|
||||||
|
"sol_gear",
|
||||||
|
"planet_gear",
|
||||||
|
"planet_gear003",
|
||||||
|
"planet_gear004",
|
||||||
|
"planet_gear005",
|
||||||
|
"planet_gear002",
|
||||||
|
"body_up"
|
||||||
|
],
|
||||||
|
"planet_gear": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"planet_gear003": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"planet_gear004": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"planet_gear005": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"planet_gear002": [
|
||||||
|
"body_down",
|
||||||
|
"sol_gear",
|
||||||
|
"output_shaft"
|
||||||
|
],
|
||||||
|
"body_up": [
|
||||||
|
"body_down",
|
||||||
|
"output_shaft",
|
||||||
|
"bolt",
|
||||||
|
"bolt2",
|
||||||
|
"bolt3",
|
||||||
|
"bolt4"
|
||||||
|
],
|
||||||
|
"bolt": [
|
||||||
|
"body_down",
|
||||||
|
"body_up"
|
||||||
|
],
|
||||||
|
"bolt2": [
|
||||||
|
"body_down",
|
||||||
|
"body_up"
|
||||||
|
],
|
||||||
|
"bolt3": [
|
||||||
|
"body_down",
|
||||||
|
"body_up"
|
||||||
|
],
|
||||||
|
"bolt4": [
|
||||||
|
"body_down",
|
||||||
|
"body_up"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"matrixError": null
|
||||||
|
}
|
Binary file not shown.
0
test_models/env.json
Normal file
0
test_models/env.json
Normal file
4
test_models/intersection_geometry.json
Normal file
4
test_models/intersection_geometry.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"status": true,
|
||||||
|
"recalculations": null
|
||||||
|
}
|
|
@ -1,10 +1,3 @@
|
||||||
{
|
{
|
||||||
"sequences": [
|
"sequences": []
|
||||||
|
|
||||||
"body_down",
|
|
||||||
"sol_gear",
|
|
||||||
"output_shaft",
|
|
||||||
"planet_gear",
|
|
||||||
"planet_gear002"
|
|
||||||
]
|
|
||||||
}
|
}
|
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue