From d8c8bb05d9a5c1ab759e8155d10dba3a64139714 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Sun, 8 Jan 2017 00:23:24 +0100 Subject: Starting on a KiCAD ontology. o Supporting many version of a single project. --- owl/kicad.owl | 76 ++++++++++++++++++++ trygvis/eda/cli/__init__.py | 7 +- trygvis/eda/cli/db_stats.py | 2 +- trygvis/eda/cli/digikey_download_for_project.py | 15 +--- trygvis/eda/cli/eda_rdf.py | 16 +---- trygvis/eda/cli/kicad_import_project.py | 92 ++++++++++++++++++------- trygvis/eda/cli/make_bom.py | 84 +++++++++++++++++----- trygvis/eda/kicad/export/__init__.py | 2 +- trygvis/eda/kicad/rdf.py | 13 +++- 9 files changed, 228 insertions(+), 79 deletions(-) create mode 100644 owl/kicad.owl diff --git a/owl/kicad.owl b/owl/kicad.owl new file mode 100644 index 0000000..ae9c763 --- /dev/null +++ b/owl/kicad.owl @@ -0,0 +1,76 @@ +Prefix(:=) +Prefix(owl:=) +Prefix(rdf:=) +Prefix(xml:=) +Prefix(xsd:=) +Prefix(rdfs:=) +Prefix(kicad-random:=) + + +Ontology( + +Declaration(Class(:component)) +Declaration(Class(:field)) +Declaration(Class(:footprint)) +Declaration(Class(:project)) +Declaration(Class(:project-version)) +Declaration(Class(:sheet)) +Declaration(ObjectProperty(:hasComponent)) +Declaration(ObjectProperty(:hasProjectVersion)) +Declaration(ObjectProperty(:hasTimestamp)) +Declaration(DataProperty(:field-name)) +Declaration(DataProperty(:field-value)) +Declaration(DataProperty(:timestamp)) + +############################ +# Object Properties +############################ + +# Object Property: owl:topObjectProperty (owl:topObjectProperty) + +ObjectPropertyDomain(owl:topObjectProperty :project) + +# Object Property: :hasComponent (:hasComponent) + +InverseObjectProperties(:hasComponent :hasProjectVersion) +ObjectPropertyDomain(:hasComponent :project-version) +ObjectPropertyRange(:hasComponent :component) + +# Object Property: :hasProjectVersion (:hasProjectVersion) + +ObjectPropertyDomain(:hasProjectVersion :component) +ObjectPropertyRange(:hasProjectVersion :project-version) + +# Object Property: :hasTimestamp (:hasTimestamp) + +ObjectPropertyDomain(:hasTimestamp :project-version) + + +############################ +# Data Properties +############################ + +# Data Property: :field-name (:field-name) + +DataPropertyRange(:field-name xsd:string) + +# Data Property: :field-value (:field-value) + +DataPropertyRange(:field-value xsd:string) + +# Data Property: :timestamp (:timestamp) + +DataPropertyRange(:timestamp xsd:dateTime) + + + +############################ +# Classes +############################ + +# Class: :project (:project) + +AnnotationAssertion(rdfs:comment :project "KiCAD Project"@en) + + +) \ No newline at end of file diff --git a/trygvis/eda/cli/__init__.py b/trygvis/eda/cli/__init__.py index aa61021..422cf48 100644 --- a/trygvis/eda/cli/__init__.py +++ b/trygvis/eda/cli/__init__.py @@ -3,6 +3,7 @@ import logging from genericpath import isfile from os import mkdir from os.path import dirname, isdir +from typing import Callable from rdflib import store, ConjunctiveGraph, Graph, RDF, RDFS from rdflib.plugins.sparql import prepareQuery @@ -92,11 +93,11 @@ def create_graph(digikey=False, kicad=False) -> Graph: if kicad: g.bind("kicad", kicad_rdf.KICAD) - g.bind("kicad-type", kicad_rdf.KICAD_TYPE) + g.bind("kicad-random", kicad_rdf.KICAD_RANDOM) return g -def write_graph(gen_g: Graph, filename: str = None, force_write: bool = False): +def write_graph(gen_g: Callable[[], Graph], filename: str = None, force_write: bool = False): if filename is not None: if force_write or not isfile(filename): parent = dirname(filename) @@ -129,7 +130,7 @@ _initNs = { "dk-part": digikey_rdf.DIGIKEY_PART, "dk-p-c": digikey_rdf.DIGIKEY_PRODUCT_CATEGORY, "kicad": kicad_rdf.KICAD, - "kicad-type": kicad_rdf.KICAD_TYPE} + "kicad-random": kicad_rdf.KICAD_RANDOM} def sparql(g: Graph, query: str, init_bindings=None): diff --git a/trygvis/eda/cli/db_stats.py b/trygvis/eda/cli/db_stats.py index babc53b..430c1f3 100755 --- a/trygvis/eda/cli/db_stats.py +++ b/trygvis/eda/cli/db_stats.py @@ -6,7 +6,7 @@ def run(args: object): res = cli.sparql(g, """ SELECT ?project ?label WHERE { - ?project a kicad-type:project + ?project a kicad:project OPTIONAL { ?project rdfs:label ?label } diff --git a/trygvis/eda/cli/digikey_download_for_project.py b/trygvis/eda/cli/digikey_download_for_project.py index 02e3cb1..a7d93e7 100755 --- a/trygvis/eda/cli/digikey_download_for_project.py +++ b/trygvis/eda/cli/digikey_download_for_project.py @@ -24,20 +24,7 @@ def work(project_url, force, g): db = DigikeyDatabase() download_category_tree(db, client) - # Dump project: - # SELECT ?project - # WHERE { - # ?project a kicad-type:project . - # b:rateboard-2_a ? ?. - # ?s ?predicate ?object . - # } - - # allComponentsQ = prepareQuery('SELECT ?ref ?value WHERE { ?project_url a kicad-type:project . ?project_url kicad:component ?o . ?o rdfs:label ?ref . ?o kicad:value ?value . }', initNs = initNs) - # - # for row in g.query(allComponentsQ, initBindings={'project_url': project_url}): - # print("ref=%s, value=%s" % (row.ref, row.value)) - - res = cli.sparql(g, "SELECT ?project WHERE {?project a kicad-type:project}") + res = cli.sparql(g, "SELECT ?project WHERE {?project a kicad:project}") print("Found %d projects in database" % len(res)) for row in res: print("project: %s" % row.project) diff --git a/trygvis/eda/cli/eda_rdf.py b/trygvis/eda/cli/eda_rdf.py index 0df66ac..ff11fda 100644 --- a/trygvis/eda/cli/eda_rdf.py +++ b/trygvis/eda/cli/eda_rdf.py @@ -1,6 +1,7 @@ from trygvis.eda.cli.digikey_download_for_project import DigikeyDownloadForProjectCommand from trygvis.eda.cli.init import InitCommand from trygvis.eda.cli.kicad_import_project import KicadImportProjectCommand +from trygvis.eda.cli.make_bom import MakeBomCommand from . import * @@ -61,19 +62,6 @@ class DigikeyDownloadAttributeTypesForCategory(CliCommand): digikey_download_attribute_types_for_category.run(args.category, args.sub_category, args.output, args) -class MakeBom(CliCommand): - def __init__(self): - super().__init__("make-bom", "Create a BOM for a project with all info for each part.") - - def run(self, argv): - p = argparse.ArgumentParser(prog=self.key, description=self.description) - p.add_argument("--schematic", required=True) - args = p.parse_args(argv) - - from trygvis.eda.cli import make_bom - make_bom.run(args.schematic) - - def main(): initialize() @@ -81,7 +69,7 @@ def main(): AddToDb(), InitCommand(), DbStats(), - MakeBom(), + MakeBomCommand(), KicadImportProjectCommand(), DigikeyDownloadForProjectCommand(), DigikeyDownloadAttributeTypesForCategory(), diff --git a/trygvis/eda/cli/kicad_import_project.py b/trygvis/eda/cli/kicad_import_project.py index 288b7fc..422f24a 100755 --- a/trygvis/eda/cli/kicad_import_project.py +++ b/trygvis/eda/cli/kicad_import_project.py @@ -1,10 +1,11 @@ import os.path from operator import attrgetter import itertools +from uuid import uuid4, UUID +import datetime import rdflib from rdflib import Literal, URIRef -from rdflib.namespace import RDF, RDFS # from ..kicad import rdf as kicad_rdf from . import * @@ -27,6 +28,9 @@ class KicadImportProjectCommand(CliCommand): def run(args): config = read_config() + version = None + timestamp = None # type: datetime.datetime + if args.input is "-": src = sys.stdin else: @@ -40,30 +44,49 @@ def run(args): raise CliException("No such file: %s. Did you export the BOM?" % filename) src = open(filename, 'r') + s = os.stat(filename) + timestamp = datetime.datetime.fromtimestamp(s.st_mtime) project_url = config['project']['url'] + if version is None: + if timestamp is None: + version = uuid4() + timestamp = datetime.datetime.now() + else: + from random import Random + r = Random(timestamp.year + timestamp.month + timestamp.day + timestamp.hour + timestamp.minute + + timestamp.second + timestamp.microsecond) + s = ["0123456789abcdef"[int(r.random() * 16)] for _ in range(0, 32)] + version = UUID(''.join(s)) + + info("Version: %s, timestamp=%s" % (version, timestamp)) + with src: - project = export_to_graph(src, project_url) + (version, project_g) = export_to_graph(src, project_url, version, timestamp) - debug('Loaded %s tuples' % len(project)) + debug('Loaded %s tuples' % len(project_g)) + + project_g.add((URIRef(project_url), RDF.type, kicad_rdf.KICAD['project'])) + project_g.add((URIRef(project_url), kicad_rdf.KICAD['version'], version)) if args.output_dir is not None: parent = args.output_dir if not os.path.exists(parent): os.mkdir(parent) - output_file = os.path.join(parent, "project.ttl") - with open(output_file, 'wb') as dst: - project.serialize(destination=dst, encoding='utf-8', format='turtle') + + v = timestamp.strftime("%Y-%m-%d %H:%M:%S") + output_file = os.path.join(parent, "project-%s.ttl" % v) + write_graph(lambda: project_g, filename=output_file, force_write=True) def import_project(g): - for idx, t in enumerate(project.triples((None, None, None))): + for idx, t in enumerate(project_g.triples((None, None, None))): g.add(t) with_database(import_project) -def export_to_graph(src: object, project_url: str) -> rdflib.Graph: +def export_to_graph(src: object, project_url: str, version: str, timestamp: datetime.datetime) -> (str, rdflib.Graph): export = Export.from_xml_file(src) # print('components:') @@ -135,8 +158,11 @@ def export_to_graph(src: object, project_url: str) -> rdflib.Graph: g = create_graph(kicad=True) - project = URIRef(project_url) - g.add((project, RDF.type, kicad_rdf.KICAD_TYPE.project)) + version_uri = URIRef('urn:uuid:%s#' % version) + g.add((version_uri, RDF.type, kicad_rdf.KICAD["project-version"])) + g.add((version_uri, RDFS.label, Literal('Version: %s' % timestamp.strftime("%Y-%m-%d %H:%M:%S")))) + g.add((version_uri, kicad_rdf.KICAD['version-of'], URIRef(project_url))) + g.add((version_uri, kicad_rdf.KICAD['timestamp'], Literal(timestamp))) # The components and fields could/should have been a BNodes. Their generated names are not very nice. # TODO: try using a hash of the current value and put that under a special generated namespace. @@ -145,29 +171,43 @@ def export_to_graph(src: object, project_url: str) -> rdflib.Graph: # add_prefix(hash) => 'https://trygvis.io/purl/kicad/generated#123456' footprints = set() for c in export.components: - ns = rdflib.Namespace(project_url) + ns = rdflib.Namespace(str(version_uri)) node = ns[c.ref] - g.add((project, kicad_rdf.KICAD.component, node)) + g.add((version_uri, kicad_rdf.KICAD['component'], node)) - g.add((node, RDF.type, kicad_rdf.KICAD_TYPE.schematic_component)) + g.add((node, RDF.type, kicad_rdf.KICAD['component'])) g.add((node, RDFS.label, Literal(c.ref))) - g.add((node, kicad_rdf.KICAD.value, Literal(c.value.text))) + g.add((node, kicad_rdf.KICAD['value'], Literal(c.value.text))) footprint_uri = URIRef(kicad_rdf.KICAD_FOOTPRINT[quote_plus(c.footprint)]) - if not footprint_uri in footprints: - g.add((footprint_uri, RDF.type, kicad_rdf.KICAD_TYPE.footprint)) + if footprint_uri not in footprints: + g.add((footprint_uri, RDF.type, kicad_rdf.KICAD['footprint'])) g.add((footprint_uri, RDFS.label, Literal(c.footprint))) footprints.add(footprint_uri) - g.add((node, kicad_rdf.KICAD.footprint, footprint_uri)) + g.add((node, kicad_rdf.KICAD['footprint'], footprint_uri)) for name, value in c.fields.items(): - f = ns['%s-%s' % (c.ref, name)] - g.add((node, kicad_rdf.KICAD.field, f)) - g.add((f, RDF.type, kicad_rdf.KICAD_TYPE.field)) - g.add((f, kicad_rdf.KICAD.field_name, Literal(name))) - g.add((f, kicad_rdf.KICAD.field_value, Literal(value))) - - # TODO: serialize the data from too - - return g + # f = ns['%s-%s' % (c.ref, name)] + f = kicad_rdf.gen_random('%s-%s' % (c.ref, name)) + g.add((node, kicad_rdf.KICAD['field'], f)) + g.add((f, RDF.type, kicad_rdf.KICAD['field'])) + g.add((f, kicad_rdf.KICAD['field-name'], Literal(name))) + g.add((f, kicad_rdf.KICAD['field-value'], Literal(value))) + + d = export.design + if d is not None: + g.add((version_uri, kicad_rdf.KICAD['design-date'], Literal(d.date))) + + for s in d.sheets: + sheet_uri = kicad_rdf.gen_random(s.number) + g.add((sheet_uri, RDF.type, kicad_rdf.KICAD['sheet'])) + label = s.title_block.title if s.title_block.title is not None and len(s.title_block.title) > 0 else s.name + g.add((sheet_uri, RDFS.label, Literal(label))) + g.add((sheet_uri, kicad_rdf.KICAD['sheet-name'], Literal(s.name))) + g.add((sheet_uri, kicad_rdf.KICAD['sheet-number'], Literal(int(s.number)))) + g.add((sheet_uri, kicad_rdf.KICAD['title'], Literal(s.title_block.title))) + g.add((sheet_uri, kicad_rdf.KICAD['rev'], Literal(s.title_block.rev))) + g.add((sheet_uri, kicad_rdf.KICAD['date'], Literal(s.title_block.date))) + + return version_uri, g diff --git a/trygvis/eda/cli/make_bom.py b/trygvis/eda/cli/make_bom.py index a47e04a..1023c11 100755 --- a/trygvis/eda/cli/make_bom.py +++ b/trygvis/eda/cli/make_bom.py @@ -1,7 +1,22 @@ -from trygvis.eda import cli -import trygvis.eda.digikey.rdf as digikey_rdf +import argparse import rdflib +import trygvis.eda.digikey.rdf as digikey_rdf +from trygvis.eda import cli, EdaException + + +class MakeBomCommand(cli.CliCommand): + def __init__(self): + super().__init__("make-bom", "Create a BOM for a project with all info for each part.") + + def run(self, argv): + p = argparse.ArgumentParser(prog=self.key, description=self.description) + p.add_argument("--project") + p.add_argument("--version") + args = p.parse_args(argv) + + run(args) + class DigiKeyPart(object): def __init__(self, part_number): @@ -22,49 +37,80 @@ class Component(object): self.fields[field_name] = field_value -def run(project_url): - cli.with_database(lambda g: work(project_url, g)) +def run(args): + cli.with_database(lambda g: work(args, g)) -def work(project_url, g): +def work(args, g): + config = cli.read_config() + project_url = args.project if args.project is not None else config['project']['url'] + components = {} dk_parts = {} - cli.info("Loading components") + version = None + if args.version is not None: + version = args.version + else: + cli.info("Finding latest version") + res = cli.sparql(g, """ +SELECT +?project ?version ?timestamp +WHERE { + ?project a kicad:project . + ?version a kicad:project-version; + kicad:timestamp ?timestamp; + kicad:version-of ?project + . +} +ORDER BY DESC(?timestamp) +LIMIT 1 +""", init_bindings={"project": rdflib.URIRef(project_url)}) + + for row in res: + version = row.version + break + else: + raise EdaException("Could not find any version of project %s" % project_url) + + cli.info("Loading components for version %s" % version) res = cli.sparql(g, """ SELECT ?ref ?value WHERE { - ?project a kicad-type:project ; + ?version a kicad:project-version ; kicad:component ?cmp . - ?cmp a kicad-type:schematic_component ; - kicad:value ?value ; - rdfs:label ?ref . + ?cmp a kicad:component ; + kicad:value ?value ; + rdfs:label ?ref . } ORDER BY ?ref -""", init_bindings={"project": rdflib.URIRef(project_url)}) +""", init_bindings={"version": rdflib.URIRef(version)}) for row in res: c = Component(row.ref, row.value) components[row.ref] = c cli.info('ref=%s, value=%s' % (c.ref, c.value)) + if len(components) == 0: + raise EdaException("Could not find any components") + cli.info("Loading custom component attributes") res = cli.sparql(g, """ SELECT ?ref ?field ?field_name ?field_value WHERE { - ?project a kicad-type:project ; + ?version a kicad:project-version ; kicad:component ?cmp . - ?cmp a kicad-type:schematic_component ; + ?cmp a kicad:component ; rdfs:label ?ref ; kicad:field ?field . - ?field a kicad-type:field ; kicad:field_name ?field_name . - ?field a kicad-type:field ; kicad:field_value ?field_value . + ?field a kicad:field ; kicad:field_name ?field_name . + ?field a kicad:field ; kicad:field_value ?field_value . } ORDER BY ?ref ?field_name -""", init_bindings={"project": rdflib.URIRef(project_url)}) +""", init_bindings={"version": rdflib.URIRef(version)}) for row in res: c = components[row.ref] @@ -78,7 +124,7 @@ SELECT ?ref ?part_number ?type ?value ?attr_type WHERE { ?project kicad:component ?cmp . - ?cmp a kicad-type:schematic_component ; + ?cmp a kicad:component ; rdfs:label ?ref ; kicad:field ?d . ?d kicad:field_name "digikey" ; @@ -114,7 +160,7 @@ SELECT # * WHERE { ?project kicad:component ?cmp . - ?cmp a kicad-type:schematic_component ; + ?cmp a kicad:component ; rdfs:label ?ref ; kicad:field ?dk_field . optional { @@ -138,7 +184,7 @@ WHERE { # ?case_type a dk:attributeType ; dk:value ?case_url . # } . -# VALUES (?project_url) { () } . +# VALUES (?project_url) { (...) } . # VALUES (?package_type) { (dk-attr-type:pv16) } . # VALUES (?case_type) { (dk-attr-type:pv1291) } . } diff --git a/trygvis/eda/kicad/export/__init__.py b/trygvis/eda/kicad/export/__init__.py index 64b82d2..9f3e96e 100644 --- a/trygvis/eda/kicad/export/__init__.py +++ b/trygvis/eda/kicad/export/__init__.py @@ -174,7 +174,7 @@ class Sheet(object): def __init__(self, number, name, title_block): self.number = number self.name = name - self.title_block = title_block + self.title_block = title_block # type: TitleBlock @staticmethod def from_xml(s): diff --git a/trygvis/eda/kicad/rdf.py b/trygvis/eda/kicad/rdf.py index 34b73e1..2984fcd 100644 --- a/trygvis/eda/kicad/rdf.py +++ b/trygvis/eda/kicad/rdf.py @@ -1,8 +1,19 @@ import rdflib +from trygvis.eda import EdaException KICAD = rdflib.Namespace("https://trygvis/purl/kicad#") -KICAD_TYPE = rdflib.Namespace("https://trygvis/purl/kicad-type#") +KICAD_RANDOM = rdflib.Namespace("https://trygvis/purl/kicad-random/") KICAD_FOOTPRINT = rdflib.Namespace("https://trygvis/purl/kicad-footprints#") # Namespace for all unknown kicad boards KICAD_BOARD = rdflib.Namespace("https://trygvis/purl/kicad-board#") + + +def gen_random(key: object): + s = str(key) + + if len(s) == 0: + raise EdaException("Can't generate a random URI for empty strings.") + + s = str(abs(hash(s))) + return KICAD_RANDOM.term(s) -- cgit v1.2.3