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
__pycache__
__pycache__/
*.idea
@ -7,6 +7,7 @@ build/
dist/
*.egg-info/
.mypy_cache/
.pytest_cache/
*.swp
*.swo

View File

@ -6,13 +6,13 @@
### Links
- [Source repository](https://mpxd.net/code/jan/g85)
- [PyPI](https://pypi.org/project/g85)
- [Github mirror](https://github.com/anewusername/g85)
## Installation
Requirements:
* python >= 3.7 (written and tested with 3.9)
* numpy
* python >= 3.10 (written and tested with 3.11)
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 .read import read
from .write import write
from .main import (
Map as Map,
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
from collections import Counter
from dataclasses import dataclass, field
@ -8,25 +7,25 @@ from itertools import chain
@dataclass
class Device:
BinType: str
NullBin: Union[str, int]
ProductId: Optional[str] = None
LotId: Optional[str] = None
WaferSize: Optional[float] = None
CreateDate: Optional[datetime.datetime] = None
DeviceSizeX: Optional[float] = None
DeviceSizeY: Optional[float] = None
SupplierName: Optional[str] = None
OriginLocation: Optional[int] = None
NullBin: str | int
ProductId: str | None = None
LotId: str | None = None
WaferSize: float | None = None
CreateDate: datetime.datetime | None = None
DeviceSizeX: float | None = None
DeviceSizeY: float | None = None
SupplierName: str | None = None
OriginLocation: int | None = None
MapType: str = 'Array'
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?
map: Union[List[List[int]], List[List[str]]] = field(default_factory=list) # The actual map
# Map attribs: MapName, MapVersion
# SupplierData attribs: ProductCode, RecipeName
bin_pass: dict[int | str, bool] = field(default_factory=dict) # Is this bin passing?
map: list[list[int]] | list[list[str]] = field(default_factory=list) # The actual map
data_misc: dict[str, str] = field(default_factory=dict) # <Data attribs>
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
def Rows(self) -> int:
@ -46,9 +45,9 @@ class Device:
class Map:
xmlns: str = 'http://www.semi.org'
FormatRevision: str = "SEMI G85 0703"
SubstrateType: Optional[str] = None
SubstrateId: Optional[str] = None
SubstrateType: str | None = None
SubstrateId: str | None = None
devices: List[Device] = field(default_factory=list)
misc: Dict[str, str] = field(default_factory=dict) # Any unexpected fields go here
devices: list[Device] = field(default_factory=list)
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 datetime
from dataclasses import fields
@ -10,7 +10,7 @@ from .main import Map, Device
logger = logging.getLogger(__name__)
def read(stream: TextIO) -> List[Map]:
def read(stream: TextIO) -> list[Map]:
tree = ElementTree.parse(stream)
el_root = tree.getroot()
@ -21,7 +21,7 @@ def read(stream: TextIO) -> List[Map]:
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)]
maps = []
for el_map in el_root:
@ -41,7 +41,7 @@ def read_wmaps(el_root: ElementTree.Element) -> List[Map]:
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)]
devices = []
for el_device in el_map:
@ -50,7 +50,7 @@ def read_devices(el_map: ElementTree.Element) -> List[Device]:
continue
bin_type = el_device.attrib['BinType']
null_bin: Union[int, str]
null_bin: int | str
if bin_type == 'Decimal':
null_bin = int(el_device.attrib['NullBin'])
else:
@ -58,22 +58,24 @@ def read_devices(el_map: ElementTree.Element) -> List[Device]:
device = Device(BinType=bin_type, NullBin=null_bin)
val: Any
for key, val in el_device.attrib.items():
if key in ('BinType', 'NullBin'):
continue
parsed_val: Any
if key in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'):
val = float(val)
parsed_val = float(val)
elif key in ('OriginLocation',):
val = int(val)
parsed_val = int(val)
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():
setattr(device, key, val)
setattr(device, key, parsed_val)
else:
device.misc[key] = val
device.misc[key] = parsed_val
for el_entry in el_device:
tag = _tag(el_entry)
@ -96,7 +98,12 @@ def read_devices(el_map: ElementTree.Element) -> List[Device]:
f'with attributes {el_entry.attrib}')
continue
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:
logger.error(f'Bin code {bin_code} was repeated; ignoring later entry!')
continue
@ -104,17 +111,23 @@ def read_devices(el_map: ElementTree.Element) -> List[Device]:
device.bin_pass[bin_code] = attrib['BinQuality'].lower() == 'pass'
elif tag == 'Data':
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':
data = [[int(vv) for vv in rr] for rr in data_strs]
else:
data = data_strs
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)
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'
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:
'''
"""
Get the element's tag, excluding any namespaces.
'''
"""
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 math
from dataclasses import fields
@ -10,11 +11,14 @@ from .main import Map, Device
logger = logging.getLogger(__name__)
class G85Error(Exception):
pass
# Hack to directly pass through <![CDATA[...]]>
def _escape_cdata(text):
def _escape_cdata(text: str) -> str:
if text.startswith('<![CDATA[') and text.endswith(']]>'):
return text
else:
return _original_escape_cdata(text)
@ -41,8 +45,11 @@ def write_wmap(wmap: Map, el_root: ElementTree.Element) -> None:
map_fields = [ff.name for ff in fields(wmap)]
for field in map_fields:
if field[0].isupper():
el_map.set(field, getattr(wmap, field))
if field[0].isupper() or field == 'xmlns':
val = getattr(wmap, field)
if val is None:
continue
el_map.set(field, val)
for key, value in wmap.misc.items():
if key[0].isupper() and key in map_fields:
continue
@ -61,7 +68,7 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
# Row data prep
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'
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('BinCount', str(bin_counts[bin_code]))
el_data = ElementTree.SubElement(el_device, 'Data')
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}]]>'
# Device attribs
@ -90,6 +98,8 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
for field in dev_fields:
if field[0].isupper():
val = getattr(device, field)
if val is None:
continue
if field in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'):
val = f'{val:g}'
@ -97,6 +107,8 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
val = f'{val:d}'
elif field == 'CreateDate':
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)
@ -105,27 +117,34 @@ def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> Non
continue
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)
row_texts = []
if is_char:
data = cast(list[list[str]], data)
char_len = len(data[0][0])
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_digits = math.ceil(math.log10(max_value))
row_texts = []
for row in data:
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) + ' '
for irow in data:
row_text = ' '.join(str(vv).zfill(max_digits) for vv in irow) + ' '
row_texts.append(row_text)
if is_char:
return row_texts, char_len
else:
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',
],
)