Compare commits

...

28 Commits

Author SHA1 Message Date
90e30821eb bump version to v0.7 2024-07-29 21:54:00 -07:00
b3a4862e46 remove unused numpy dependency 2024-07-29 21:53:06 -07:00
763f09051a add ruff config 2024-07-29 21:51:04 -07:00
eaef972c88 flatten some indentation 2024-07-29 21:50:57 -07:00
5fb229c09f improve type annotations 2024-07-29 21:50:44 -07:00
a3773df853 use subclassed Exception 2024-07-29 21:50:31 -07:00
0f68796831 use double quotes for docstring 2024-07-29 21:50:17 -07:00
deb3460df3 rename altered loop variable 2024-07-29 21:50:06 -07:00
afa2f0259d re-export using "import x as x" 2024-07-29 21:49:50 -07:00
b450fd10fe Add github mirror and bump python version 2024-03-30 18:17:40 -07:00
dfbb907274 add type hint 2024-03-30 18:16:51 -07:00
jan
feeaef6aa4 bump version to v0.6 2023-07-13 14:13:46 -07:00
jan
9d01997275 don't convert ascii BinCode to int 2023-07-13 14:10:49 -07:00
jan
e47bff3204 correctly output xmlns attribute 2023-07-13 14:10:31 -07:00
jan
701400d442 handle attributes on <Data> and <SupplierData> 2023-07-13 14:02:43 -07:00
jan
26ca1cd012 put rows inside <Data> when writing 2023-07-13 14:02:14 -07:00
jan
7e318b2001 bump min python version to 3.10 to support modern annotations 2023-07-13 13:25:06 -07:00
jan
4dc3a6892a modernize type annotations 2023-07-13 13:16:01 -07:00
9a35859210 Bump version to v0.5 2022-08-18 23:28:29 -07:00
cbd484db6a Move to hatch-based build 2022-08-18 23:28:06 -07:00
41d994e8fc bump version to v0.4 2021-11-12 14:21:07 -08:00
2b8b50d084 Convert BinCode to int when reading a map with decimal values 2021-11-12 14:20:35 -08:00
65533b8d99 fix getting field from wrong var 2021-11-12 14:18:10 -08:00
9ee5778933 bump version to v0.3 2021-11-12 14:05:45 -08:00
8d5f60ca72 fix incomplete commit 28eb68f57 2021-11-12 14:05:25 -08:00
6648b607ff bump version to 0.2 2021-11-12 14:01:38 -08:00
411ad512e5 Convert NullBin from int 2021-11-12 14:01:18 -08:00
28eb68f57a Avoid setting xml attribs to None 2021-11-12 14:01:00 -08:00
11 changed files with 184 additions and 121 deletions

3
.gitignore vendored
View File

@ -1,5 +1,5 @@
*.pyc *.pyc
__pycache__ __pycache__/
*.idea *.idea
@ -7,6 +7,7 @@ build/
dist/ dist/
*.egg-info/ *.egg-info/
.mypy_cache/ .mypy_cache/
.pytest_cache/
*.swp *.swp
*.swo *.swo

View File

@ -6,13 +6,13 @@
### Links ### Links
- [Source repository](https://mpxd.net/code/jan/g85) - [Source repository](https://mpxd.net/code/jan/g85)
- [PyPI](https://pypi.org/project/g85) - [PyPI](https://pypi.org/project/g85)
- [Github mirror](https://github.com/anewusername/g85)
## Installation ## Installation
Requirements: Requirements:
* python >= 3.7 (written and tested with 3.9) * python >= 3.10 (written and tested with 3.11)
* numpy
Install with pip: Install with pip:

1
g85/LICENSE.md Symbolic link
View File

@ -0,0 +1 @@
../LICENSE.md

1
g85/README.md Symbolic link
View File

@ -0,0 +1 @@
../README.md

View File

@ -1,4 +0,0 @@
""" VERSION defintion. THIS FILE IS MANUALLY PARSED BY setup.py and REQUIRES A SPECIFIC FORMAT """
__version__ = '''
0.1
'''.strip()

View File

@ -1,3 +1,9 @@
from .main import Map, Device from .main import (
from .read import read Map as Map,
from .write import write Device as Device,
)
from .read import read as read
from .write import write as write
__author__ = 'Jan Petykiewicz'
__version__ = '0.7'

View File

@ -1,4 +1,3 @@
from typing import Dict, List, Tuple, Union, Optional
import datetime import datetime
from collections import Counter from collections import Counter
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -8,25 +7,25 @@ from itertools import chain
@dataclass @dataclass
class Device: class Device:
BinType: str BinType: str
NullBin: Union[str, int] NullBin: str | int
ProductId: Optional[str] = None ProductId: str | None = None
LotId: Optional[str] = None LotId: str | None = None
WaferSize: Optional[float] = None WaferSize: float | None = None
CreateDate: Optional[datetime.datetime] = None CreateDate: datetime.datetime | None = None
DeviceSizeX: Optional[float] = None DeviceSizeX: float | None = None
DeviceSizeY: Optional[float] = None DeviceSizeY: float | None = None
SupplierName: Optional[str] = None SupplierName: str | None = None
OriginLocation: Optional[int] = None OriginLocation: int | None = None
MapType: str = 'Array' MapType: str = 'Array'
Orientation: float = 0 Orientation: float = 0
reference_xy: Optional[Tuple[int, int]] = None reference_xy: tuple[int, int] | None = None
bin_pass: Dict[Union[int, str], bool] = field(default_factory=dict) # Is this bin passing? bin_pass: dict[int | str, bool] = field(default_factory=dict) # Is this bin passing?
map: Union[List[List[int]], List[List[str]]] = field(default_factory=list) # The actual map map: list[list[int]] | list[list[str]] = field(default_factory=list) # The actual map
# Map attribs: MapName, MapVersion data_misc: dict[str, str] = field(default_factory=dict) # <Data attribs>
# SupplierData attribs: ProductCode, RecipeName supplier_data: dict[str, str] = field(default_factory=dict) # <SupplierData attribs>
misc: Dict[str, str] = field(default_factory=dict) # Any unexpected fields go here misc: dict[str, str] = field(default_factory=dict) # Any unexpected fields go here
@property @property
def Rows(self) -> int: def Rows(self) -> int:
@ -46,9 +45,9 @@ class Device:
class Map: class Map:
xmlns: str = 'http://www.semi.org' xmlns: str = 'http://www.semi.org'
FormatRevision: str = "SEMI G85 0703" FormatRevision: str = "SEMI G85 0703"
SubstrateType: Optional[str] = None SubstrateType: str | None = None
SubstrateId: Optional[str] = None SubstrateId: str | None = None
devices: List[Device] = field(default_factory=list) devices: list[Device] = field(default_factory=list)
misc: Dict[str, str] = field(default_factory=dict) # Any unexpected fields go here misc: dict[str, str] = field(default_factory=dict) # Any unexpected fields go here

View File

@ -1,4 +1,4 @@
from typing import List, Union, TextIO, Any from typing import TextIO, Any
import logging import logging
import datetime import datetime
from dataclasses import fields from dataclasses import fields
@ -10,7 +10,7 @@ from .main import Map, Device
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def read(stream: TextIO) -> List[Map]: def read(stream: TextIO) -> list[Map]:
tree = ElementTree.parse(stream) tree = ElementTree.parse(stream)
el_root = tree.getroot() el_root = tree.getroot()
@ -21,7 +21,7 @@ def read(stream: TextIO) -> List[Map]:
return maps return maps
def read_wmaps(el_root: ElementTree.Element) -> List[Map]: def read_wmaps(el_root: ElementTree.Element) -> list[Map]:
map_fields = [ff.name for ff in fields(Map)] map_fields = [ff.name for ff in fields(Map)]
maps = [] maps = []
for el_map in el_root: for el_map in el_root:
@ -41,7 +41,7 @@ def read_wmaps(el_root: ElementTree.Element) -> List[Map]:
return maps return maps
def read_devices(el_map: ElementTree.Element) -> List[Device]: def read_devices(el_map: ElementTree.Element) -> list[Device]:
dev_fields = [ff.name for ff in fields(Device)] dev_fields = [ff.name for ff in fields(Device)]
devices = [] devices = []
for el_device in el_map: for el_device in el_map:
@ -50,7 +50,7 @@ def read_devices(el_map: ElementTree.Element) -> List[Device]:
continue continue
bin_type = el_device.attrib['BinType'] bin_type = el_device.attrib['BinType']
null_bin: Union[int, str] null_bin: int | str
if bin_type == 'Decimal': if bin_type == 'Decimal':
null_bin = int(el_device.attrib['NullBin']) null_bin = int(el_device.attrib['NullBin'])
else: else:
@ -58,22 +58,24 @@ def read_devices(el_map: ElementTree.Element) -> List[Device]:
device = Device(BinType=bin_type, NullBin=null_bin) device = Device(BinType=bin_type, NullBin=null_bin)
val: Any
for key, val in el_device.attrib.items(): for key, val in el_device.attrib.items():
if key in ('BinType', 'NullBin'): if key in ('BinType', 'NullBin'):
continue continue
parsed_val: Any
if key in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'): if key in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'):
val = float(val) parsed_val = float(val)
elif key in ('OriginLocation',): elif key in ('OriginLocation',):
val = int(val) parsed_val = int(val)
elif key == 'CreateDate': elif key == 'CreateDate':
val = datetime.datetime.strptime(val + '000', '%Y%m%d%H%M%S%f') parsed_val = datetime.datetime.strptime(val + '000', '%Y%m%d%H%M%S%f')
else:
parsed_val = val
if key in dev_fields and key[0].isupper(): if key in dev_fields and key[0].isupper():
setattr(device, key, val) setattr(device, key, parsed_val)
else: else:
device.misc[key] = val device.misc[key] = parsed_val
for el_entry in el_device: for el_entry in el_device:
tag = _tag(el_entry) tag = _tag(el_entry)
@ -96,7 +98,12 @@ def read_devices(el_map: ElementTree.Element) -> List[Device]:
f'with attributes {el_entry.attrib}') f'with attributes {el_entry.attrib}')
continue continue
bin_code = attrib['BinCode'] bin_code: int | str
if bin_type == 'Decimal':
bin_code = int(attrib['BinCode'])
else:
bin_code = attrib['BinCode']
if bin_code in device.bin_pass: if bin_code in device.bin_pass:
logger.error(f'Bin code {bin_code} was repeated; ignoring later entry!') logger.error(f'Bin code {bin_code} was repeated; ignoring later entry!')
continue continue
@ -104,17 +111,23 @@ def read_devices(el_map: ElementTree.Element) -> List[Device]:
device.bin_pass[bin_code] = attrib['BinQuality'].lower() == 'pass' device.bin_pass[bin_code] = attrib['BinQuality'].lower() == 'pass'
elif tag == 'Data': elif tag == 'Data':
data_strs = [read_row(rr) for rr in el_entry] data_strs = [read_row(rr) for rr in el_entry]
data: Union[List[List[str]], List[List[int]]] data: list[list[str]] | list[list[int]]
if device.BinType == 'Decimal': if device.BinType == 'Decimal':
data = [[int(vv) for vv in rr] for rr in data_strs] data = [[int(vv) for vv in rr] for rr in data_strs]
else: else:
data = data_strs data = data_strs
device.map = data device.map = data
for key, value in attrib.items():
device.data_misc[key] = value
elif tag == 'SupplierData':
for key, value in attrib.items():
device.supplier_data[key] = value
devices.append(device) devices.append(device)
return devices return devices
def read_row(el_row: ElementTree.Element) -> List[str]: def read_row(el_row: ElementTree.Element) -> list[str]:
assert _tag(el_row) == 'Row' assert _tag(el_row) == 'Row'
row_stripped = (el_row.text or '').strip() row_stripped = (el_row.text or '').strip()
@ -126,7 +139,7 @@ def read_row(el_row: ElementTree.Element) -> List[str]:
def _tag(element: ElementTree.Element) -> str: def _tag(element: ElementTree.Element) -> str:
''' """
Get the element's tag, excluding any namespaces. Get the element's tag, excluding any namespaces.
''' """
return element.tag.split('}')[-1] return element.tag.split('}')[-1]

View File

@ -1,4 +1,5 @@
from typing import Sequence, Tuple, List, TextIO, Union from typing import TextIO, cast
from collections.abc import Sequence
import logging import logging
import math import math
from dataclasses import fields from dataclasses import fields
@ -10,12 +11,15 @@ from .main import Map, Device
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class G85Error(Exception):
pass
# Hack to directly pass through <![CDATA[...]]> # Hack to directly pass through <![CDATA[...]]>
def _escape_cdata(text): def _escape_cdata(text: str) -> str:
if text.startswith('<![CDATA[') and text.endswith(']]>'): if text.startswith('<![CDATA[') and text.endswith(']]>'):
return text return text
else: return _original_escape_cdata(text)
return _original_escape_cdata(text)
_original_escape_cdata = ElementTree._escape_cdata # type: ignore _original_escape_cdata = ElementTree._escape_cdata # type: ignore
@ -41,8 +45,11 @@ def write_wmap(wmap: Map, el_root: ElementTree.Element) -> None:
map_fields = [ff.name for ff in fields(wmap)] map_fields = [ff.name for ff in fields(wmap)]
for field in map_fields: for field in map_fields:
if field[0].isupper(): if field[0].isupper() or field == 'xmlns':
el_map.set(field, getattr(wmap, field)) val = getattr(wmap, field)
if val is None:
continue
el_map.set(field, val)
for key, value in wmap.misc.items(): for key, value in wmap.misc.items():
if key[0].isupper() and key in map_fields: if key[0].isupper() and key in map_fields:
continue continue
@ -61,7 +68,7 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
# Row data prep # Row data prep
if device.map is None: if device.map is None:
raise Exception(f'No _data for device pformat({device})') raise G85Error(f'No _data for device pformat({device})')
is_decimal = device.BinType == 'Decimal' is_decimal = device.BinType == 'Decimal'
row_texts, bin_length = prepare_data(device.map, decimal=is_decimal) row_texts, bin_length = prepare_data(device.map, decimal=is_decimal)
@ -81,8 +88,9 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
el_bin.set('BinQuality', 'Pass' if passed else 'Fail') el_bin.set('BinQuality', 'Pass' if passed else 'Fail')
el_bin.set('BinCount', str(bin_counts[bin_code])) el_bin.set('BinCount', str(bin_counts[bin_code]))
el_data = ElementTree.SubElement(el_device, 'Data')
for row_text in row_texts: for row_text in row_texts:
el_row = ElementTree.SubElement(el_device, 'Row') el_row = ElementTree.SubElement(el_data, 'Row')
el_row.text = f'<![CDATA[{row_text}]]>' el_row.text = f'<![CDATA[{row_text}]]>'
# Device attribs # Device attribs
@ -90,6 +98,8 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
for field in dev_fields: for field in dev_fields:
if field[0].isupper(): if field[0].isupper():
val = getattr(device, field) val = getattr(device, field)
if val is None:
continue
if field in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'): if field in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'):
val = f'{val:g}' val = f'{val:g}'
@ -97,6 +107,8 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
val = f'{val:d}' val = f'{val:d}'
elif field == 'CreateDate': elif field == 'CreateDate':
val = val.strftime('%Y%m%d%H%M%S%f')[:-3] val = val.strftime('%Y%m%d%H%M%S%f')[:-3]
elif field == 'NullBin' and device.BinType == 'Decimal':
val = f'{val:d}'
el_device.set(field, val) el_device.set(field, val)
@ -105,27 +117,34 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
continue continue
el_device.set(key, value) el_device.set(key, value)
for key, value in device.data_misc.items():
el_data.set(key, value)
def prepare_data(data: List[List[Union[str, int]]], decimal: bool) -> Tuple[List[str], int]: if device.supplier_data:
el_suppdata = ElementTree.SubElement(el_device, 'SupplierData')
for key, value in device.data_misc.items():
el_suppdata.set(key, value)
def prepare_data(data: list[list[str]] | list[list[int]], decimal: bool) -> tuple[list[str], int]:
is_char = isinstance(data[0][0], str) is_char = isinstance(data[0][0], str)
row_texts = []
if is_char: if is_char:
data = cast(list[list[str]], data)
char_len = len(data[0][0]) char_len = len(data[0][0])
else: for srow in data:
if char_len == 1:
row_text = ''.join(srow)
else:
row_text = ' '.join(srow) + ' '
row_texts.append(row_text)
return row_texts, char_len
else: # noqa: RET505
data = cast(list[list[int]], data)
max_value = max(max(rr) for rr in data) max_value = max(max(rr) for rr in data)
max_digits = math.ceil(math.log10(max_value)) max_digits = math.ceil(math.log10(max_value))
for irow in data:
row_texts = [] row_text = ' '.join(str(vv).zfill(max_digits) for vv in irow) + ' '
for row in data: row_texts.append(row_text)
if is_char and char_len == 1:
row_text = ''.join(row)
elif is_char:
row_text = ' '.join(row) + ' '
else:
row_text = ' '.join(str(vv).zfill(max_digits) for vv in row) + ' '
row_texts.append(row_text)
if is_char:
return row_texts, char_len
else:
return row_texts, max_digits return row_texts, max_digits

78
pyproject.toml Normal file
View File

@ -0,0 +1,78 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "g85"
description = "G85 wafer map reader / writer"
readme = "README.md"
license = { file = "LICENSE.md" }
authors = [
{ name="Jan Petykiewicz", email="jan@mpxd.net" },
]
homepage = "https://mpxd.net/code/jan/g85"
repository = "https://mpxd.net/code/jan/g85"
keywords = [
"design",
"CAD",
"EDA",
"electronics",
"photonics",
"IC",
"mask",
"wafer",
"map",
"G85",
"wmap",
]
classifiers = [
"Programming Language :: Python :: 3",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: Manufacturing",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
]
requires-python = ">=3.10"
dynamic = ["version"]
dependencies = [
]
[tool.hatch.version]
path = "g85/__init__.py"
[tool.ruff]
exclude = [
".git",
"dist",
]
line-length = 145
indent-width = 4
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
lint.select = [
"NPY", "E", "F", "W", "B", "ANN", "UP", "SLOT", "SIM", "LOG",
"C4", "ISC", "PIE", "PT", "RET", "TCH", "PTH", "INT",
"ARG", "PL", "R", "TRY",
"G010", "G101", "G201", "G202",
"Q002", "Q003", "Q004",
]
lint.ignore = [
#"ANN001", # No annotation
"ANN002", # *args
"ANN003", # **kwargs
"ANN401", # Any
"ANN101", # self: Self
"SIM108", # single-line if / else assignment
"RET504", # x=y+z; return x
"PIE790", # unnecessary pass
"ISC003", # non-implicit string concatenation
"C408", # dict(x=y) instead of {'x': y}
"PLR09", # Too many xxx
"PLR2004", # magic number
"PLC0414", # import x as x
"TRY003", # Long exception message
]

View File

@ -1,51 +0,0 @@
#!/usr/bin/env python3
from setuptools import setup, find_packages
with open('README.md', 'r') as f:
long_description = f.read()
with open('g85/VERSION.py', 'rt') as f:
version = f.readlines()[2].strip()
setup(
name='g85',
version=version,
description='G85 wafer map reader / writer',
long_description=long_description,
long_description_content_type='text/markdown',
author='Jan Petykiewicz',
author_email='jan@mpxd.net',
url='https://mpxd.net/code/jan/g85',
packages=find_packages(),
package_data={
'g85': ['py.typed'],
},
install_requires=[
'numpy',
],
classifiers=[
'Programming Language :: Python :: 3',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: Manufacturing',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
],
keywords=[
'design',
'CAD',
'EDA',
'electronics',
'photonics',
'IC',
'mask',
'wafer',
'map',
'G85',
'wmap',
],
)