initial commit
This commit is contained in:
commit
0552a0a75f
9 changed files with 1032 additions and 0 deletions
3
g85/__init__.py
Normal file
3
g85/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .main import Map, Device
|
||||
from .read import read
|
||||
from .write import write
|
||||
54
g85/main.py
Normal file
54
g85/main.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from typing import Dict, List, Tuple, Union, Optional
|
||||
import datetime
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass, field
|
||||
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
|
||||
MapType: str = 'Array'
|
||||
Orientation: float = 0
|
||||
reference_xy: Optional[Tuple[int, int]] = 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
|
||||
|
||||
misc: Dict[str, str] = field(default_factory=dict) # Any unexpected fields go here
|
||||
|
||||
@property
|
||||
def Rows(self) -> int:
|
||||
return len(self.map)
|
||||
|
||||
@property
|
||||
def Columns(self) -> int:
|
||||
if self.Rows == 0:
|
||||
return 0
|
||||
return len(self.map[0])
|
||||
|
||||
def bin_counts(self) -> Counter:
|
||||
return Counter(chain(*self.map))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Map:
|
||||
xmlns: str = 'http://www.semi.org'
|
||||
FormatRevision: str = "SEMI G85 0703"
|
||||
SubstrateType: Optional[str] = None
|
||||
SubstrateId: Optional[str] = None
|
||||
|
||||
devices: List[Device] = field(default_factory=list)
|
||||
misc: Dict[str, str] = field(default_factory=dict) # Any unexpected fields go here
|
||||
|
||||
132
g85/read.py
Normal file
132
g85/read.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
from typing import List, Union, TextIO, Any
|
||||
import logging
|
||||
import datetime
|
||||
from dataclasses import fields
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from .main import Map, Device
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def read(stream: TextIO) -> List[Map]:
|
||||
tree = ElementTree.parse(stream)
|
||||
el_root = tree.getroot()
|
||||
|
||||
if _tag(el_root) != 'Maps':
|
||||
logger.warning(f'Root tag is "{_tag(el_root)}", expected "Maps"')
|
||||
|
||||
maps = read_wmaps(el_root)
|
||||
return maps
|
||||
|
||||
|
||||
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:
|
||||
if _tag(el_map) != 'Map':
|
||||
logger.warning(f'Skipping map-level tag "{_tag(el_map)}"')
|
||||
continue
|
||||
|
||||
wmap = Map()
|
||||
|
||||
for key, val in el_map.attrib.items():
|
||||
if key in map_fields and key[0].isupper():
|
||||
setattr(wmap, key, val)
|
||||
else:
|
||||
wmap.misc[key] = val
|
||||
wmap.devices = read_devices(el_map)
|
||||
maps.append(wmap)
|
||||
return maps
|
||||
|
||||
|
||||
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:
|
||||
if _tag(el_device) != 'Device':
|
||||
logger.warning(f'Skipping device-level tag "{_tag(el_device)}"')
|
||||
continue
|
||||
|
||||
bin_type = el_device.attrib['BinType']
|
||||
null_bin: Union[int, str]
|
||||
if bin_type == 'Decimal':
|
||||
null_bin = int(el_device.attrib['NullBin'])
|
||||
else:
|
||||
null_bin = el_device.attrib['NullBin']
|
||||
|
||||
device = Device(BinType=bin_type, NullBin=null_bin)
|
||||
|
||||
val: Any
|
||||
for key, val in el_device.attrib.items():
|
||||
if key in ('BinType', 'NullBin'):
|
||||
continue
|
||||
|
||||
if key in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'):
|
||||
val = float(val)
|
||||
elif key in ('OriginLocation',):
|
||||
val = int(val)
|
||||
elif key == 'CreateDate':
|
||||
val = datetime.datetime.strptime(val + '000', '%Y%m%d%H%M%S%f')
|
||||
|
||||
if key in dev_fields and key[0].isupper():
|
||||
setattr(device, key, val)
|
||||
else:
|
||||
device.misc[key] = val
|
||||
|
||||
for el_entry in el_device:
|
||||
tag = _tag(el_entry)
|
||||
attrib = el_entry.attrib
|
||||
if tag == 'ReferenceDevice':
|
||||
if device.reference_xy is not None:
|
||||
logger.warning('Duplicate ReferenceDevice entry; overwriting!')
|
||||
|
||||
xy = (attrib.get('ReferenceDeviceX', None),
|
||||
attrib.get('ReferenceDeviceY', None))
|
||||
|
||||
if xy[0] is None or xy[1] is None:
|
||||
logger.error('Malformed ReferenceDevice, ignoring!')
|
||||
continue
|
||||
|
||||
device.reference_xy = (int(xy[0]), int(xy[1]))
|
||||
elif tag == 'Bin':
|
||||
if 'BinCode' not in attrib:
|
||||
logger.error(f'Bin without any associated BinCode, '
|
||||
f'with attributes {el_entry.attrib}')
|
||||
continue
|
||||
|
||||
bin_code = attrib['BinCode']
|
||||
if bin_code in device.bin_pass:
|
||||
logger.error(f'Bin code {bin_code} was repeated; ignoring later entry!')
|
||||
continue
|
||||
|
||||
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]]]
|
||||
if device.BinType == 'Decimal':
|
||||
data = [[int(vv) for vv in rr] for rr in data_strs]
|
||||
else:
|
||||
data = data_strs
|
||||
device.map = data
|
||||
devices.append(device)
|
||||
return devices
|
||||
|
||||
|
||||
def read_row(el_row: ElementTree.Element) -> List[str]:
|
||||
assert _tag(el_row) == 'Row'
|
||||
|
||||
row_stripped = (el_row.text or '').strip()
|
||||
if ' ' in row_stripped or '\t' in row_stripped:
|
||||
row_data = row_stripped.split()
|
||||
else:
|
||||
row_data = list(row_stripped)
|
||||
return row_data
|
||||
|
||||
|
||||
def _tag(element: ElementTree.Element) -> str:
|
||||
'''
|
||||
Get the element's tag, excluding any namespaces.
|
||||
'''
|
||||
return element.tag.split('}')[-1]
|
||||
131
g85/write.py
Normal file
131
g85/write.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
from typing import Sequence, Tuple, List, TextIO, Union
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import fields
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from .main import Map, Device
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Hack to directly pass through <![CDATA[...]]>
|
||||
def _escape_cdata(text):
|
||||
if text.startswith('<![CDATA[') and text.endswith(']]>'):
|
||||
return text
|
||||
else:
|
||||
return _original_escape_cdata(text)
|
||||
|
||||
|
||||
_original_escape_cdata = ElementTree._escape_cdata # type: ignore
|
||||
ElementTree._escape_cdata = _escape_cdata # type: ignore
|
||||
####
|
||||
|
||||
|
||||
def write(maps: Sequence[Map], stream: TextIO) -> None:
|
||||
el_root = ElementTree.Element('Maps')
|
||||
|
||||
for wmap in maps:
|
||||
write_wmap(wmap, el_root)
|
||||
|
||||
tree = ElementTree.ElementTree(element=el_root)
|
||||
ElementTree.indent(tree)
|
||||
tree.write(stream)
|
||||
|
||||
|
||||
def write_wmap(wmap: Map, el_root: ElementTree.Element) -> None:
|
||||
el_map = ElementTree.SubElement(el_root, 'Map')
|
||||
|
||||
write_devices(wmap.devices, el_map)
|
||||
|
||||
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))
|
||||
for key, value in wmap.misc.items():
|
||||
if key[0].isupper() and key in map_fields:
|
||||
continue
|
||||
el_map.set(key, value)
|
||||
|
||||
|
||||
def write_devices(devices: Sequence[Device], el_map: ElementTree.Element) -> None:
|
||||
for device in devices:
|
||||
el_device = ElementTree.SubElement(el_map, 'Device')
|
||||
|
||||
# ReferenceDevice
|
||||
if device.reference_xy is not None:
|
||||
el_ref = ElementTree.SubElement(el_device, 'ReferenceDevice')
|
||||
el_ref.set('ReferenceDeviceX', str(device.reference_xy[0]))
|
||||
el_ref.set('ReferenceDeviceY', str(device.reference_xy[1]))
|
||||
|
||||
# Row data prep
|
||||
if device.map is None:
|
||||
raise Exception(f'No _data for device pformat({device})')
|
||||
|
||||
is_decimal = device.BinType == 'Decimal'
|
||||
row_texts, bin_length = prepare_data(device.map, decimal=is_decimal)
|
||||
|
||||
# Bins
|
||||
if not device.bin_pass:
|
||||
logger.warning('No bins were provided!')
|
||||
|
||||
bin_counts = device.bin_counts()
|
||||
|
||||
for bin_code, passed in device.bin_pass.items():
|
||||
el_bin = ElementTree.SubElement(el_device, 'Bin')
|
||||
if is_decimal:
|
||||
el_bin.set('BinCode', str(bin_code).zfill(bin_length))
|
||||
else:
|
||||
el_bin.set('BinCode', str(bin_code))
|
||||
el_bin.set('BinQuality', 'Pass' if passed else 'Fail')
|
||||
el_bin.set('BinCount', str(bin_counts[bin_code]))
|
||||
|
||||
for row_text in row_texts:
|
||||
el_row = ElementTree.SubElement(el_device, 'Row')
|
||||
el_row.text = f'<![CDATA[{row_text}]]>'
|
||||
|
||||
# Device attribs
|
||||
dev_fields = [ff.name for ff in fields(device)]
|
||||
for field in dev_fields:
|
||||
if field[0].isupper():
|
||||
val = getattr(device, field)
|
||||
|
||||
if field in ('WaferSize', 'DeviceSizeX', 'DeviceSizeY', 'Orientation'):
|
||||
val = f'{val:g}'
|
||||
elif field in ('OriginLocation',):
|
||||
val = f'{val:d}'
|
||||
elif field == 'CreateDate':
|
||||
val = val.strftime('%Y%m%d%H%M%S%f')[:-3]
|
||||
|
||||
el_device.set(field, val)
|
||||
|
||||
for key, value in device.misc.items():
|
||||
if key[0].isupper() and key in dev_fields:
|
||||
continue
|
||||
el_device.set(key, value)
|
||||
|
||||
|
||||
def prepare_data(data: List[List[Union[str, int]]], decimal: bool) -> Tuple[List[str], int]:
|
||||
is_char = isinstance(data[0][0], str)
|
||||
|
||||
if is_char:
|
||||
char_len = len(data[0][0])
|
||||
else:
|
||||
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) + ' '
|
||||
row_texts.append(row_text)
|
||||
|
||||
if is_char:
|
||||
return row_texts, char_len
|
||||
else:
|
||||
return row_texts, max_digits
|
||||
Loading…
Add table
Add a link
Reference in a new issue